| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- // ==UserScript==
- // @name SharePoint Stream Helper
- // @namespace https://github.com/sp-stream-helper
- // @version 1.1
- // @description 一键复制 SharePoint Stream videomanifest 请求为 cURL 格式
- // @match https://*.sharepoint.com/personal/*/_layouts/*/stream.aspx*
- // @match https://*.sharepoint.com/sites/*/_layouts/*/stream.aspx*
- // @grant none
- // @run-at document-start
- // ==/UserScript==
- (function () {
- 'use strict';
- let capturedManifest = null;
- let btnReady = false;
- // ========================================
- // 1. 在 document-start 阶段拦截 fetch/XHR
- // @grant none 让脚本在页面上下文运行
- // ========================================
- // --- 拦截 fetch ---
- const origFetch = window.fetch;
- window.fetch = function (input, init) {
- try {
- const url = typeof input === 'string' ? input : (input?.url || '');
- const method = (init?.method || input?.method || 'GET').toUpperCase();
- const headers = extractHeaders(init?.headers || input?.headers);
- maybeCapture(method, url, headers);
- } catch (e) { /* 不影响原始请求 */ }
- return origFetch.apply(this, arguments);
- };
- // --- 拦截 XMLHttpRequest ---
- const origOpen = XMLHttpRequest.prototype.open;
- const origSetHeader = XMLHttpRequest.prototype.setRequestHeader;
- const origSend = XMLHttpRequest.prototype.send;
- XMLHttpRequest.prototype.open = function (method, url) {
- this._spH = { method: (method || 'GET').toUpperCase(), url: url || '', headers: {} };
- return origOpen.apply(this, arguments);
- };
- XMLHttpRequest.prototype.setRequestHeader = function (name, value) {
- if (this._spH) this._spH.headers[name] = value;
- return origSetHeader.apply(this, arguments);
- };
- XMLHttpRequest.prototype.send = function () {
- if (this._spH) maybeCapture(this._spH.method, this._spH.url, this._spH.headers);
- return origSend.apply(this, arguments);
- };
- // ========================================
- // 2. 辅助函数
- // ========================================
- function extractHeaders(raw) {
- const h = {};
- if (!raw) return h;
- if (raw instanceof Headers) {
- raw.forEach((v, k) => { h[k] = v; });
- } else if (Array.isArray(raw)) {
- raw.forEach(([k, v]) => { h[k] = v; });
- } else if (typeof raw === 'object') {
- Object.keys(raw).forEach(k => { h[k] = raw[k]; });
- }
- return h;
- }
- function maybeCapture(method, url, headers) {
- if (!url || !url.includes('videomanifest')) return;
- if (method === 'OPTIONS') return;
- // 必须有 pac token
- const pacKey = Object.keys(headers).find(k => k.toLowerCase() === 'x-spopactoken');
- if (!pacKey) return;
- capturedManifest = { method, url, headers: { ...headers }, time: new Date().toLocaleTimeString() };
- console.log('[SP Helper] ✅ 捕获 videomanifest!', url.substring(0, 80) + '...');
- if (btnReady) updateButton();
- }
- // ========================================
- // 3. 备用方案: PerformanceObserver 监听
- // 如果 fetch/XHR 拦截失败,从 performance entries 拿 URL
- // 再从 g_fileInfo 拿 PAC token 拼 cURL
- // ========================================
- function startPerfObserver() {
- try {
- const obs = new PerformanceObserver((list) => {
- for (const entry of list.getEntries()) {
- if (entry.name && entry.name.includes('videomanifest') && !entry.name.includes('OPTIONS')) {
- if (!capturedManifest) {
- // 从 g_fileInfo 获取 PAC token
- const pacToken = getPacToken();
- if (pacToken) {
- capturedManifest = {
- method: 'GET',
- url: entry.name,
- headers: { 'x-spopactoken': pacToken },
- time: new Date().toLocaleTimeString(),
- source: 'PerformanceObserver',
- };
- console.log('[SP Helper] ✅ 通过 PerformanceObserver 捕获!');
- if (btnReady) updateButton();
- }
- }
- }
- }
- });
- obs.observe({ type: 'resource', buffered: true });
- } catch (e) {
- console.log('[SP Helper] PerformanceObserver 不可用:', e.message);
- }
- }
- function getPacToken() {
- // 方法1: g_fileInfo
- try {
- if (window.g_fileInfo) {
- const code = window.g_fileInfo['.driveAccessCode'] ||
- window.g_fileInfo['.driveAccessCodeV21'];
- if (code) return code;
- }
- } catch (e) {}
- // 方法2: 从页面 HTML 中提取
- try {
- const html = document.documentElement.innerHTML;
- const m = html.match(/["']\.driveAccessCode["']\s*:\s*["']([^"']+)["']/);
- if (m) return m[1];
- } catch (e) {}
- return null;
- }
- // ========================================
- // 4. 生成 cURL
- // ========================================
- function toCurl(req) {
- const parts = [`curl '${req.url}'`];
- // 从捕获的 headers 中提取 pac token 和 origin/referer
- let pacToken = '';
- let origin = '';
- let referer = '';
- for (const [name, value] of Object.entries(req.headers)) {
- const lower = name.toLowerCase();
- if (lower === 'x-spopactoken') pacToken = value;
- if (lower === 'origin') origin = value;
- if (lower === 'referer') referer = value;
- }
- // 固定 headers (模拟浏览器 Chrome 146)
- parts.push(` -H 'accept: */*'`);
- parts.push(` -H 'accept-language: zh-CN,zh;q=0.9'`);
- if (origin) parts.push(` -H 'origin: ${origin}'`);
- parts.push(` -H 'priority: u=1, i'`);
- if (referer) parts.push(` -H 'referer: ${referer}'`);
- parts.push(` -H 'sec-ch-ua: "Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146"'`);
- parts.push(` -H 'sec-ch-ua-mobile: ?0'`);
- parts.push(` -H 'sec-ch-ua-platform: "Windows"'`);
- parts.push(` -H 'sec-fetch-dest: empty'`);
- parts.push(` -H 'sec-fetch-mode: cors'`);
- parts.push(` -H 'sec-fetch-site: cross-site'`);
- parts.push(` -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36'`);
- if (pacToken) {
- const safeVal = pacToken.replace(/'/g, "'\\''");
- parts.push(` -H 'x-spopactoken: ${safeVal}'`);
- }
- return parts.join(' \\\n');
- }
- // ========================================
- // 5. UI
- // ========================================
- function createButton() {
- const btn = document.createElement('div');
- btn.id = 'sp-helper-btn';
- btn.textContent = '⏳ 等待视频播放...';
- Object.assign(btn.style, {
- position: 'fixed',
- bottom: '20px',
- right: '20px',
- zIndex: '2147483647',
- padding: '10px 18px',
- borderRadius: '8px',
- background: '#555',
- color: '#ccc',
- fontSize: '14px',
- fontFamily: 'Consolas, "Courier New", monospace',
- cursor: 'not-allowed',
- boxShadow: '0 2px 12px rgba(0,0,0,0.4)',
- transition: 'all 0.3s',
- userSelect: 'none',
- });
- btn.addEventListener('click', onCopy);
- document.body.appendChild(btn);
- btnReady = true;
- return btn;
- }
- function updateButton() {
- let btn = document.getElementById('sp-helper-btn');
- if (!btn) btn = createButton();
- if (capturedManifest) {
- const src = capturedManifest.source ? ` [${capturedManifest.source}]` : '';
- btn.innerHTML = `📋 复制 cURL <span style="font-size:11px;opacity:0.6">${capturedManifest.time}${src}</span>`;
- btn.style.background = '#0078d4';
- btn.style.color = '#fff';
- btn.style.cursor = 'pointer';
- }
- }
- function onCopy() {
- if (!capturedManifest) return;
- const curl = toCurl(capturedManifest);
- navigator.clipboard.writeText(curl).then(() => {
- showCopied();
- }).catch(() => {
- // fallback
- const ta = document.createElement('textarea');
- ta.value = curl;
- ta.style.cssText = 'position:fixed;left:-9999px';
- document.body.appendChild(ta);
- ta.select();
- document.execCommand('copy');
- ta.remove();
- showCopied();
- });
- console.log('[SP Helper] cURL 已复制 (' + curl.length + ' chars)');
- }
- function showCopied() {
- const btn = document.getElementById('sp-helper-btn');
- if (!btn) return;
- const prev = btn.innerHTML;
- btn.innerHTML = '✅ 已复制!';
- btn.style.background = '#107c10';
- setTimeout(() => {
- btn.innerHTML = prev;
- btn.style.background = '#0078d4';
- }, 2000);
- }
- // ========================================
- // 6. 启动
- // ========================================
- // DOM ready 后创建按钮和启动 PerformanceObserver
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', init);
- } else {
- init();
- }
- function init() {
- createButton();
- startPerfObserver();
- // 如果已经捕获到了(fetch/XHR 拦截在 document-start 就生效)
- if (capturedManifest) updateButton();
- console.log('[SP Helper] 已加载,等待 videomanifest 请求...');
- }
- })();
|