Spaces:
Sleeping
Sleeping
Commit Β·
6b506cb
1
Parent(s): 1ea78e7
Capture PO token via in-page fetch/XHR hook (fix: token not captured)
Browse filesYouTube fetches media via fetch()/SABR, which the MV3 service-worker webRequest path can
miss. Add MAIN-world content_hook.js that patches fetch + XHR to read the gvs `pot` from
videoplayback URLs (and visitor_data from ytcfg), bridged to session storage via
content_bridge.js. Background now raises session-storage access level for content scripts
and keeps a broadened webRequest capture as backup. Popup gives a precise diagnostic
(media seen vs. token present). Bump to 1.1.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- extension/README.md +15 -0
- extension/background.js +14 -13
- extension/content_bridge.js +25 -0
- extension/content_hook.js +56 -0
- extension/manifest.json +16 -1
- extension/popup.js +13 -5
extension/README.md
CHANGED
|
@@ -52,6 +52,21 @@ It just surfaces these for one-click copy; it sends nothing anywhere.
|
|
| 52 |
events so Gradio's Svelte bindings register the change. Requires the `*://*.hf.space/*`
|
| 53 |
host permission.
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
## Caveats
|
| 56 |
|
| 57 |
- **Unpublished / sideloaded** β you load it yourself; trust accordingly. The code is
|
|
|
|
| 52 |
events so Gradio's Svelte bindings register the change. Requires the `*://*.hf.space/*`
|
| 53 |
host permission.
|
| 54 |
|
| 55 |
+
## Troubleshooting
|
| 56 |
+
|
| 57 |
+
**"No PO token yet" even though the video is playing.** YouTube fetches media via
|
| 58 |
+
`fetch`/XHR (SABR), so the token is captured by a page-level hook (`content_hook.js`), not
|
| 59 |
+
only the service worker. If it still doesn't appear:
|
| 60 |
+
|
| 61 |
+
- **Reload the video page** after installing the extension (the hook must be in place
|
| 62 |
+
*before* the player starts).
|
| 63 |
+
- Press **play** and let it **buffer a few seconds**, then click **Refresh** in the popup.
|
| 64 |
+
- If the popup says *"Media requests were seen but none carried a PO token,"* your current
|
| 65 |
+
session isn't attaching one β try a **logged-out Incognito** window (with the extension
|
| 66 |
+
allowed in Incognito), or just use **cookies** instead.
|
| 67 |
+
- Make sure the extension is **allowed in Incognito** if you test there
|
| 68 |
+
(`chrome://extensions` β Details β Allow in Incognito).
|
| 69 |
+
|
| 70 |
## Caveats
|
| 71 |
|
| 72 |
- **Unpublished / sideloaded** β you load it yourself; trust accordingly. The code is
|
extension/background.js
CHANGED
|
@@ -1,27 +1,28 @@
|
|
| 1 |
-
// Service worker
|
| 2 |
-
//
|
| 3 |
-
//
|
| 4 |
-
//
|
| 5 |
-
//
|
| 6 |
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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) {
|
|
|
|
| 1 |
+
// Service worker. Two jobs:
|
| 2 |
+
// 1. Raise the session-storage access level so the youtube.com content-script bridge
|
| 3 |
+
// (an "untrusted" context) can persist captured values for the popup to read.
|
| 4 |
+
// 2. A secondary, read-only webRequest capture of the gvs PO token from
|
| 5 |
+
// *.googlevideo.com/videoplayback requests (the page-level fetch/XHR hook is primary).
|
| 6 |
|
| 7 |
+
try {
|
| 8 |
+
chrome.storage.session.setAccessLevel({ accessLevel: "TRUSTED_AND_UNTRUSTED_CONTEXTS" });
|
| 9 |
+
} catch (e) {}
|
| 10 |
+
|
| 11 |
+
const FILTER = { urls: ["*://*.googlevideo.com/*"] };
|
| 12 |
|
| 13 |
chrome.webRequest.onBeforeRequest.addListener(
|
| 14 |
(details) => {
|
| 15 |
try {
|
| 16 |
+
if (details.url.indexOf("videoplayback") === -1) return;
|
| 17 |
+
chrome.storage.session.set({ gvAnySeen: true, lastSeen: Date.now() });
|
| 18 |
const u = new URL(details.url);
|
| 19 |
const pot = u.searchParams.get("pot");
|
| 20 |
if (!pot) return;
|
| 21 |
const client = u.searchParams.get("c") || "";
|
| 22 |
+
chrome.storage.session.get(["gvsClient"]).then((cur) => {
|
|
|
|
| 23 |
const haveWeb = cur.gvsClient === "WEB";
|
| 24 |
if (!haveWeb || client === "WEB") {
|
| 25 |
+
chrome.storage.session.set({ gvsPot: pot, gvsClient: client, potTime: Date.now() });
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
}
|
| 27 |
});
|
| 28 |
} catch (e) {
|
extension/content_bridge.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Isolated-world bridge: receives postMessages from the MAIN-world hook and persists the
|
| 2 |
+
// captured values to session storage (which the popup reads). Needs the background script
|
| 3 |
+
// to have raised the session-storage access level for untrusted contexts.
|
| 4 |
+
window.addEventListener("message", function (e) {
|
| 5 |
+
if (e.source !== window) return;
|
| 6 |
+
const d = e.data;
|
| 7 |
+
if (!d || d.__potgrab !== true) return;
|
| 8 |
+
|
| 9 |
+
if (d.type === "seen") {
|
| 10 |
+
chrome.storage.session.set({ gvAnySeen: true, lastSeen: Date.now() });
|
| 11 |
+
} else if (d.type === "pot" && d.data && d.data.pot) {
|
| 12 |
+
chrome.storage.session.get(["gvsClient"]).then(function (cur) {
|
| 13 |
+
const haveWeb = cur.gvsClient === "WEB";
|
| 14 |
+
if (!haveWeb || d.data.c === "WEB") {
|
| 15 |
+
chrome.storage.session.set({
|
| 16 |
+
gvsPot: d.data.pot,
|
| 17 |
+
gvsClient: d.data.c || "",
|
| 18 |
+
potTime: Date.now(),
|
| 19 |
+
});
|
| 20 |
+
}
|
| 21 |
+
});
|
| 22 |
+
} else if (d.type === "visitor" && d.data && d.data.v) {
|
| 23 |
+
chrome.storage.session.set({ visitorData: d.data.v });
|
| 24 |
+
}
|
| 25 |
+
});
|
extension/content_hook.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// MAIN-world hook (runs in the page's JS context at document_start).
|
| 2 |
+
//
|
| 3 |
+
// Modern YouTube fetches media via fetch()/XHR to *.googlevideo.com/videoplayback?...&pot=...
|
| 4 |
+
// (the MV3 service-worker webRequest path can miss these). We patch fetch + XHR to observe
|
| 5 |
+
// those request URLs, pull the gvs `pot`, and post it to the isolated bridge script. We
|
| 6 |
+
// also report visitor_data once it's available. We only observe β requests pass through
|
| 7 |
+
// untouched.
|
| 8 |
+
(function () {
|
| 9 |
+
function report(type, data) {
|
| 10 |
+
try { window.postMessage({ __potgrab: true, type: type, data: data }, "*"); } catch (e) {}
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
function scanUrl(url) {
|
| 14 |
+
try {
|
| 15 |
+
if (!url || String(url).indexOf("videoplayback") === -1) return;
|
| 16 |
+
report("seen", null);
|
| 17 |
+
const u = new URL(url, location.href);
|
| 18 |
+
const pot = u.searchParams.get("pot");
|
| 19 |
+
if (pot) report("pot", { pot: pot, c: u.searchParams.get("c") || "" });
|
| 20 |
+
} catch (e) {}
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
try {
|
| 24 |
+
const origFetch = window.fetch;
|
| 25 |
+
if (origFetch) {
|
| 26 |
+
window.fetch = function (input) {
|
| 27 |
+
try { scanUrl(typeof input === "string" ? input : (input && input.url)); } catch (e) {}
|
| 28 |
+
return origFetch.apply(this, arguments);
|
| 29 |
+
};
|
| 30 |
+
}
|
| 31 |
+
} catch (e) {}
|
| 32 |
+
|
| 33 |
+
try {
|
| 34 |
+
const origOpen = XMLHttpRequest.prototype.open;
|
| 35 |
+
XMLHttpRequest.prototype.open = function (method, url) {
|
| 36 |
+
try { scanUrl(url); } catch (e) {}
|
| 37 |
+
return origOpen.apply(this, arguments);
|
| 38 |
+
};
|
| 39 |
+
} catch (e) {}
|
| 40 |
+
|
| 41 |
+
function reportVisitor() {
|
| 42 |
+
try {
|
| 43 |
+
if (window.ytcfg && typeof ytcfg.get === "function") {
|
| 44 |
+
const v = ytcfg.get("VISITOR_DATA");
|
| 45 |
+
if (v) { report("visitor", { v: v }); return true; }
|
| 46 |
+
}
|
| 47 |
+
} catch (e) {}
|
| 48 |
+
return false;
|
| 49 |
+
}
|
| 50 |
+
if (!reportVisitor()) {
|
| 51 |
+
let tries = 0;
|
| 52 |
+
const iv = setInterval(function () {
|
| 53 |
+
if (reportVisitor() || ++tries > 30) clearInterval(iv);
|
| 54 |
+
}, 1000);
|
| 55 |
+
}
|
| 56 |
+
})();
|
extension/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
{
|
| 2 |
"manifest_version": 3,
|
| 3 |
"name": "TutorialMaker PO Token Grabber",
|
| 4 |
-
"version": "1.
|
| 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": [
|
|
@@ -10,6 +10,21 @@
|
|
| 10 |
"*://*.hf.space/*"
|
| 11 |
],
|
| 12 |
"background": { "service_worker": "background.js" },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
"action": {
|
| 14 |
"default_popup": "popup.html",
|
| 15 |
"default_title": "PO Token Grabber"
|
|
|
|
| 1 |
{
|
| 2 |
"manifest_version": 3,
|
| 3 |
"name": "TutorialMaker PO Token Grabber",
|
| 4 |
+
"version": "1.1.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": [
|
|
|
|
| 10 |
"*://*.hf.space/*"
|
| 11 |
],
|
| 12 |
"background": { "service_worker": "background.js" },
|
| 13 |
+
"content_scripts": [
|
| 14 |
+
{
|
| 15 |
+
"matches": ["*://*.youtube.com/*"],
|
| 16 |
+
"js": ["content_hook.js"],
|
| 17 |
+
"run_at": "document_start",
|
| 18 |
+
"world": "MAIN",
|
| 19 |
+
"all_frames": true
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"matches": ["*://*.youtube.com/*"],
|
| 23 |
+
"js": ["content_bridge.js"],
|
| 24 |
+
"run_at": "document_start",
|
| 25 |
+
"all_frames": true
|
| 26 |
+
}
|
| 27 |
+
],
|
| 28 |
"action": {
|
| 29 |
"default_popup": "popup.html",
|
| 30 |
"default_title": "PO Token Grabber"
|
extension/popup.js
CHANGED
|
@@ -88,6 +88,7 @@ async function load() {
|
|
| 88 |
"gvsClient",
|
| 89 |
"potTime",
|
| 90 |
"visitorData",
|
|
|
|
| 91 |
]);
|
| 92 |
const visitor = liveVisitor || store.visitorData || "";
|
| 93 |
|
|
@@ -100,11 +101,18 @@ async function load() {
|
|
| 100 |
document.getElementById("fillSpace").disabled = spaceTabs.length === 0;
|
| 101 |
|
| 102 |
const notes = [];
|
| 103 |
-
if (!
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.");
|
|
|
|
| 88 |
"gvsClient",
|
| 89 |
"potTime",
|
| 90 |
"visitorData",
|
| 91 |
+
"gvAnySeen",
|
| 92 |
]);
|
| 93 |
const visitor = liveVisitor || store.visitorData || "";
|
| 94 |
|
|
|
|
| 101 |
document.getElementById("fillSpace").disabled = spaceTabs.length === 0;
|
| 102 |
|
| 103 |
const notes = [];
|
| 104 |
+
if (!visitor) notes.push("No visitor data yet β open the popup once on the YouTube tab.");
|
| 105 |
+
if (!store.gvsPot) {
|
| 106 |
+
if (store.gvAnySeen) {
|
| 107 |
+
notes.push("Media requests were seen but none carried a PO token β this session may "
|
| 108 |
+
+ "not attach one. Try a logged-out Incognito window, reload the video, or use "
|
| 109 |
+
+ "cookies instead.");
|
| 110 |
+
} else if (isYouTube(tab && tab.url)) {
|
| 111 |
+
notes.push("No media captured yet β press play and let the video buffer a few "
|
| 112 |
+
+ "seconds, then Refresh. If a video is already playing, reload the page.");
|
| 113 |
+
} else {
|
| 114 |
+
notes.push("Open a YouTube video and press play to capture a PO token.");
|
| 115 |
+
}
|
| 116 |
}
|
| 117 |
if (store.gvsPot && store.gvsClient && store.gvsClient !== "WEB") {
|
| 118 |
notes.push("Captured client is '" + store.gvsClient + "', not WEB β use the normal web player.");
|