Allow users to use In-Site player on patronreact.com
اعتبارا من
// ==UserScript==
// @name PRPlayer Userscript
// @namespace http://patronreact.net/
// @version 1.2
// @description Allow users to use In-Site player on patronreact.com
// @match *://*/*
// @grant GM_xmlhttpRequest
// @connect mux.com
// @connect stream.mux.com
// @connect stats.mux.com
// @run-at document-start
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const TARGET_DOMAIN = 'mux.com';
const REFERER_URL = 'https://www.patreon.com/';
// Utility to get string URL
function getUrlString(url) {
if (!url) return '';
return (url instanceof URL) ? url.href : String(url);
}
// --- 1. Intercept Fetch ---
const originalFetch = window.fetch;
window.fetch = async function (...args) {
let urlStr = '';
let options = args[1] || {};
if (args[0] instanceof Request) {
urlStr = args[0].url;
options = {
method: args[0].method,
headers: args[0].headers,
body: args[0].body,
credentials: args[0].credentials,
mode: args[0].mode
};
} else {
urlStr = getUrlString(args[0]);
}
if (urlStr.includes(TARGET_DOMAIN)) {
return new Promise((resolve, reject) => {
let reqHeaders = {};
// Copy original headers intelligently
if (args[0] instanceof Request && args[0].headers) {
args[0].headers.forEach((value, key) => { reqHeaders[key] = value; });
} else if (options.headers instanceof Headers) {
options.headers.forEach((value, key) => { reqHeaders[key] = value; });
} else if (options.headers) {
reqHeaders = { ...options.headers };
}
reqHeaders['Referer'] = REFERER_URL;
reqHeaders['Origin'] = 'https://www.patreon.com'; // Add Origin to be safe
GM_xmlhttpRequest({
method: options.method || 'GET',
url: urlStr,
headers: reqHeaders,
data: options.body || null,
responseType: 'arraybuffer',
onload: function (response) {
const responseOptions = {
status: response.status,
statusText: response.statusText,
headers: new Headers()
};
if (response.responseHeaders) {
response.responseHeaders.trim().split(/[\r\n]+/).forEach((line) => {
const parts = line.split(': ');
const key = parts.shift();
const value = parts.join(': ');
if (key) responseOptions.headers.append(key, value);
});
}
resolve(new Response(response.response, responseOptions));
},
onerror: () => reject(new TypeError('Fetch failed (GM_xmlhttpRequest error)')),
ontimeout: () => reject(new TypeError('Fetch failed (Timeout)')),
onabort: () => reject(new DOMException('Aborted', 'AbortError'))
});
});
}
return originalFetch.apply(this, args);
};
// --- 2. Intercept XMLHttpRequest ---
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;
const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
const urlStr = getUrlString(url);
this._isMuxRequest = urlStr.includes(TARGET_DOMAIN);
this._reqUrl = urlStr;
this._reqMethod = method;
this._reqHeaders = {};
return originalOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
if (this._isMuxRequest) {
this._reqHeaders[header] = value;
}
return originalSetRequestHeader.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function (body) {
if (!this._isMuxRequest) {
return originalSend.apply(this, arguments);
}
const xhr = this; // native XHR instance
this._reqHeaders['Referer'] = REFERER_URL;
this._reqHeaders['Origin'] = 'https://www.patreon.com';
// Helper to safely redefine properties
const defineSafe = (prop, val) => {
try {
Object.defineProperty(xhr, prop, { value: val, configurable: true, writable: true });
} catch (e) {
// If it fails, fallback to standard assignment (might fail if read-only)
try { xhr[prop] = val; } catch (e2) {}
}
};
// Dispatch synthetic events on the real XHR
const dispatch = (type) => {
xhr.dispatchEvent(new Event(type));
if (typeof xhr[`on${type}`] === 'function') {
xhr[`on${type}`](new Event(type));
}
};
GM_xmlhttpRequest({
method: this._reqMethod || 'GET',
url: this._reqUrl,
headers: this._reqHeaders,
data: body,
responseType: this.responseType || 'arraybuffer', // Very important for video blobs
onreadystatechange: function (response) {
defineSafe('readyState', response.readyState);
defineSafe('status', response.status);
defineSafe('statusText', response.statusText);
defineSafe('responseURL', response.finalUrl || xhr._reqUrl);
if (response.readyState === 4) {
defineSafe('response', response.response);
if (xhr.responseType === '' || xhr.responseType === 'text') {
defineSafe('responseText', response.responseText);
}
// Mock headers
xhr.getAllResponseHeaders = () => response.responseHeaders || '';
xhr.getResponseHeader = (name) => {
if (!response.responseHeaders) return null;
const regex = new RegExp('^' + name + ': (.*)$', 'im');
const match = response.responseHeaders.match(regex);
return match ? match[1] : null;
};
}
dispatch('readystatechange');
},
onload: function (response) {
dispatch('load');
dispatch('loadend');
},
onerror: function () {
dispatch('error');
dispatch('loadend');
},
onabort: function () {
dispatch('abort');
dispatch('loadend');
},
ontimeout: function () {
dispatch('timeout');
dispatch('loadend');
},
onprogress: function (response) {
if (xhr.onprogress) {
// Try to construct a ProgressEvent if possible
try {
const progressEvent = new ProgressEvent('progress', {
lengthComputable: response.lengthComputable,
loaded: response.loaded,
total: response.total
});
xhr.dispatchEvent(progressEvent);
xhr.onprogress(progressEvent);
} catch (e) {
dispatch('progress'); // fallback
}
}
}
});
};
})();