// ==UserScript==
// @name 猫抓 - 深度搜索
// @namespace https://bmmmd.com
// @version 2.4.3.0
// @description 猫抓扩展提取出来的深度搜索脚本。
// @author bmm
// @match http://*/*
// @match https://*/*
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant none
// @run-at document-start
// @license GPL v3
// ==/UserScript==
(function () {
const _log = console.log;
_log("start search.js");
const CATCH_SEARCH_DEBUG = false;
const filter = new Set();
// 拦截JSON.parse 分析内容
const _JSONparse = JSON.parse;
JSON.parse = function () {
let data = _JSONparse.apply(this, arguments);
findMedia(data);
return data;
}
// 反检测
JSON.parse.toString = function () {
return _JSONparse.toString();
}
async function findMedia(data, depth = 0) {
CATCH_SEARCH_DEBUG && _log(data);
let index = 0;
if (!data) { return; }
if (data instanceof Array && data.length == 16) {
const isKey = data.every(function (value) {
return typeof value == 'number' && value <= 256
});
if (isKey) {
postData({ action: "catCatchAddKey", key: data, href: location.href, ext: "key" });
return;
}
}
for (let key in data) {
if (index != 0) { depth = 0; } index++;
if (typeof data[key] == "object") {
// 查找疑似key
if (data[key] instanceof Array && data[key].length == 16) {
const isKey = data[key].every(function (value) {
return typeof value == 'number' && value <= 256
});
isKey && postData({ action: "catCatchAddKey", key: data[key], href: location.href, ext: "key" });
continue;
}
if (depth > 10) { continue; } // 防止死循环 最大深度
findMedia(data[key], ++depth);
continue;
}
if (typeof data[key] == "string") {
if (isUrl(data[key])) {
let ext = getExtension(data[key]);
ext && postData({ action: "catCatchAddMedia", url: data[key], href: location.href, ext: ext });
continue;
}
if (data[key].substring(0, 7).toUpperCase() == "#EXTM3U") {
isFullM3u8(data[key]) && toUrl(data[key]);
continue;
}
if (data[key].substring(0, 17).toLowerCase() == "data:application/") {
const text = getDataM3U8(data[key].substring(17));
text && toUrl(text);
continue;
}
if (data[key].toLowerCase().includes("urn:mpeg:dash:schema:mpd")) {
toUrl(data[key], "mpd");
continue;
}
if (CATCH_SEARCH_DEBUG && data[key].includes("manifest")) {
_log(data);
}
}
}
}
// 拦截 XHR 分析内容
const _xhrOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method) {
method = method.toUpperCase();
CATCH_SEARCH_DEBUG && _log(this);
this.addEventListener("readystatechange", function (event) {
CATCH_SEARCH_DEBUG && _log(this);
if (this.status != 200) { return; }
// 查找疑似key
if (this.responseType == "arraybuffer" && this.response?.byteLength && this.response.byteLength == 16) {
postData({ action: "catCatchAddKey", key: this.response, href: location.href, ext: "key" });
}
if (this.response == "" || typeof this.response != "string") { return; }
if (this.response.substring(0, 17).toLowerCase() == "data:application/") {
const text = this.response.substring(17);
toUrl(getDataM3U8(text));
return;
}
if (this.responseURL.substring(0, 17).toLowerCase() == "data:application/") {
const text = getDataM3U8(this.responseURL.substring(17));
text && toUrl(text);
return;
}
if (isUrl(this.response)) {
const ext = getExtension(this.response);
ext && postData({ action: "catCatchAddMedia", url: this.response, href: location.href, ext: ext });
return;
}
if (this.response.toUpperCase().includes("#EXTM3U")) {
if (this.response.substring(0, 7) == "#EXTM3U") {
if (method == "GET") {
postData({ action: "catCatchAddMedia", url: this.responseURL, href: location.href, ext: "m3u8" });
return;
}
isFullM3u8(this.response) && toUrl(this.response);
return;
}
if (isJSON(this.response)) {
if (method == "GET") {
postData({ action: "catCatchAddMedia", url: this.responseURL, href: location.href, ext: "json" });
return;
}
toUrl(this.response, "json");
return;
}
}
const isJson = isJSON(this.response);
if (isJson) {
findMedia(isJson);
return;
}
});
_xhrOpen.apply(this, arguments);
}
// 反检测
XMLHttpRequest.prototype.open.toString = function () {
return _xhrOpen.toString();
}
// 拦截 fetch 分析内容
const _fetch = window.fetch;
window.fetch = async function (input, init) {
const response = await _fetch.apply(this, arguments);
const clone = response.clone();
CATCH_SEARCH_DEBUG && _log(response);
response.arrayBuffer()
.then(arrayBuffer => {
CATCH_SEARCH_DEBUG && _log({ arrayBuffer, input });
if (arrayBuffer.byteLength == 16) {
postData({ action: "catCatchAddKey", key: arrayBuffer, href: location.href, ext: "key" });
return;
}
let text = new TextDecoder().decode(arrayBuffer);
if (text == "") { return; }
if (typeof input == "object") { input = input.url; }
let isJson = isJSON(text);
if (isJson) {
findMedia(isJson);
return;
}
if (text.substring(0, 7).toUpperCase() == "#EXTM3U") {
if (init?.method == undefined || (init.method && init.method.toUpperCase() == "GET")) {
postData({ action: "catCatchAddMedia", url: input, href: location.href, ext: "m3u8" });
return;
}
isFullM3u8(text) && toUrl(text);
return;
}
if (text.substring(0, 17).toLowerCase() == "data:application/") {
const text = getDataM3U8(text.substring(0, 17));
text && toUrl(text);
return;
}
});
return clone;
}
// 反检测
window.fetch.toString = function () {
return _fetch.toString();
}
// 拦截 Array.prototype.slice
const _slice = Array.prototype.slice;
Array.prototype.slice = function (start, end) {
let data = _slice.apply(this, arguments);
if (end == 16 && this.length == 32) {
for (let item of data) {
if (typeof item != "number" || item > 255) { return data; }
}
postData({ action: "catCatchAddKey", key: data, href: location.href, ext: "key" });
}
return data;
}
// 反检测
Array.prototype.slice.toString = function () {
return _slice.toString();
}
// 拦截 window.btoa / window.atob
const _btoa = window.btoa;
window.btoa = function (data) {
const base64 = _btoa.apply(this, arguments);
CATCH_SEARCH_DEBUG && _log(base64, data, base64.length);
if (base64.length == 24 && base64.substring(22, 24) == "==") {
postData({ action: "catCatchAddKey", key: base64, href: location.href, ext: "base64Key" });
}
if (data.toUpperCase().substring(0, 7) == "#EXTM3U" && isFullM3u8(data)) {
toUrl(data);
}
return base64;
}
// 反检测
window.btoa.toString = function () {
return _btoa.toString();
}
const _atob = window.atob;
window.atob = function (base64) {
const data = _atob.apply(this, arguments);
CATCH_SEARCH_DEBUG && _log(base64, data, base64.length);
if (base64.length == 24 && base64.substring(22, 24) == "==") {
postData({ action: "catCatchAddKey", key: base64, href: location.href, ext: "base64Key" });
}
if (data.toUpperCase().substring(0, 7) == "#EXTM3U" && isFullM3u8(data)) {
toUrl(data);
}
return data;
}
// 反检测
window.atob.toString = function () {
return _atob.toString();
}
function isUrl(str) {
return /^http[s]*:\/\/.+/i.test(str);
}
function isFullM3u8(text) {
let tsLists = text.split("\n");
for (let ts of tsLists) {
if (ts[0] == "#") { continue; }
if (isUrl(ts)) { return true; }
return false;
}
return false;
}
function isJSON(str) {
if (typeof str == "object") {
return str;
}
if (typeof str == "string") {
try {
return _JSONparse(str);
} catch (e) { return false; }
}
return false;
}
function getExtension(str) {
let ext;
try { ext = new URL(str); } catch (e) { return undefined; }
ext = ext.pathname.split(".");
if (ext.length == 1) { return undefined; }
ext = ext[ext.length - 1].toLowerCase();
if (ext == "m3u8" ||
ext == "m3u" ||
ext == "mpd" ||
ext == "mp4" ||
ext == "mp3" ||
ext == "key"
) { return ext; }
return false;
}
function toUrl(text, ext = "m3u8") {
let url = URL.createObjectURL(new Blob([new TextEncoder("utf-8").encode(text)]));
postData({ action: "catCatchAddMedia", url: url, href: location.href, ext: ext });
}
function getDataM3U8(text) {
const type = ["vnd.apple.mpegurl", "x-mpegurl", "mpegurl"];
let isM3U8 = false;
for (let item of type) {
if (text.substring(0, item.length).toLowerCase() == item) {
text = text.substring(item.length + 1);
isM3U8 = true;
break;
}
}
if (!isM3U8) { return false; }
if (text.substring(0, 7).toLowerCase() == "base64,") {
return window.atob(text.substring(7));
}
return text;
}
function postData(data) {
const key = data.url ? data.url : data.key;
if (filter.has(key)) { return false; }
filter.add(key);
data.requestId = Date.now().toString() + filter.size;
window.postMessage(data);
}
})();