This commit is contained in:
ChuXun
2025-10-19 20:55:27 +08:00
parent e879ccefb3
commit 53f9554f38
99 changed files with 22308 additions and 2 deletions

395
js/init.js Normal file
View File

@@ -0,0 +1,395 @@
// 低版本chrome manifest v3协议 会有 getMessage 函数不存在的bug
if (chrome.i18n.getMessage === undefined) {
chrome.i18n.getMessage = (key) => key;
fetch(chrome.runtime.getURL("_locales/zh_CN/messages.json")).then(res => res.json()).then(data => {
chrome.i18n.getMessage = (key) => data[key].messages;
}).catch((e) => { console.error(e); });
}
/**
* 部分修改版chrome 不存在 chrome.downloads API
* 例如 夸克浏览器
* 使用传统下载方式下载 但无法监听 无法另存为 无法判断下载是否失败 唉~
*/
if (!chrome.downloads) {
chrome.downloads = {
download: function (options, callback) {
let a = document.createElement('a');
a.href = options.url;
a.download = options.filename;
a.click();
a = null;
callback && callback();
},
onChanged: { addListener: function () { } },
showDefaultFolder: function () { },
show: function () { },
}
}
// 兼容 114版本以下没有chrome.sidePanel
if (!chrome.sidePanel || !chrome.sidePanel.setPanelBehavior) {
chrome.sidePanel = {
setOptions: function (options) { },
setPanelBehavior: function (options) { },
}
}
// 简写翻译函数
const i18n = new Proxy(chrome.i18n.getMessage, {
get: function (target, key) {
return chrome.i18n.getMessage(key);
}
});
// 全局变量
var G = {};
G.initSyncComplete = false;
G.initLocalComplete = false;
// 缓存数据
var cacheData = { init: true };
G.blackList = new Set(); // 正则屏蔽资源列表
G.blockUrlSet = new Set(); // 屏蔽网址列表
G.requestHeaders = new Map(); // 临时储存请求头
G.urlMap = new Map(); // url查重map
G.deepSearchTemporarilyClose = null; // 深度搜索临时变量
// 初始化当前tabId
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
if (tabs[0] && tabs[0].id) {
G.tabId = tabs[0].id;
} else {
G.tabId = -1;
}
});
// 手机浏览器
G.isMobile = /Mobile|Android|iPhone|iPad/i.test(navigator.userAgent);
// 所有设置变量 默认值
G.OptionLists = {
Ext: [
{ "ext": "flv", "size": 0, "state": true },
{ "ext": "hlv", "size": 0, "state": true },
{ "ext": "f4v", "size": 0, "state": true },
{ "ext": "mp4", "size": 0, "state": true },
{ "ext": "mp3", "size": 0, "state": true },
{ "ext": "wma", "size": 0, "state": true },
{ "ext": "wav", "size": 0, "state": true },
{ "ext": "m4a", "size": 0, "state": true },
{ "ext": "ts", "size": 0, "state": false },
{ "ext": "webm", "size": 0, "state": true },
{ "ext": "ogg", "size": 0, "state": true },
{ "ext": "ogv", "size": 0, "state": true },
{ "ext": "acc", "size": 0, "state": true },
{ "ext": "mov", "size": 0, "state": true },
{ "ext": "mkv", "size": 0, "state": true },
{ "ext": "m4s", "size": 0, "state": true },
{ "ext": "m3u8", "size": 0, "state": true },
{ "ext": "m3u", "size": 0, "state": true },
{ "ext": "mpeg", "size": 0, "state": true },
{ "ext": "avi", "size": 0, "state": true },
{ "ext": "wmv", "size": 0, "state": true },
{ "ext": "asf", "size": 0, "state": true },
{ "ext": "movie", "size": 0, "state": true },
{ "ext": "divx", "size": 0, "state": true },
{ "ext": "mpeg4", "size": 0, "state": true },
{ "ext": "vid", "size": 0, "state": true },
{ "ext": "aac", "size": 0, "state": true },
{ "ext": "mpd", "size": 0, "state": true },
{ "ext": "weba", "size": 0, "state": true },
{ "ext": "opus", "size": 0, "state": true },
{ "ext": "srt", "size": 0, "state": false },
{ "ext": "vtt", "size": 0, "state": false },
],
Type: [
{ "type": "audio/*", "size": 0, "state": true },
{ "type": "video/*", "size": 0, "state": true },
{ "type": "application/ogg", "size": 0, "state": true },
{ "type": "application/vnd.apple.mpegurl", "size": 0, "state": true },
{ "type": "application/x-mpegurl", "size": 0, "state": true },
{ "type": "application/mpegurl", "size": 0, "state": true },
{ "type": "application/octet-stream-m3u8", "size": 0, "state": true },
{ "type": "application/dash+xml", "size": 0, "state": true },
{ "type": "application/m4s", "size": 0, "state": true },
],
Regex: [
{ "type": "ig", "regex": "https://cache\\.video\\.[a-z]*\\.com/dash\\?tvid=.*", "ext": "json", "state": false },
{ "type": "ig", "regex": ".*\\.bilivideo\\.(com|cn).*\\/live-bvc\\/.*m4s", "ext": "", "blackList": true, "state": false },
],
TitleName: false,
Player: "",
ShowWebIco: !G.isMobile,
MobileUserAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1",
m3u8dl: 0,
m3u8dlArg: `"\${url}" --save-dir "%USERPROFILE%\\Downloads\\m3u8dl" --save-name "\${title}_\${now}" \${referer|exists:'-H "Referer:*"'} \${cookie|exists:'-H "Cookie:*"'} --no-log`,
m3u8dlConfirm: false,
playbackRate: 2,
copyM3U8: "${url}",
copyMPD: "${url}",
copyOther: "${url}",
autoClearMode: 1,
catDownload: false,
saveAs: false,
userAgent: "",
downFileName: "${title}.${ext}",
css: "",
checkDuplicates: true,
enable: true,
downActive: !G.isMobile, // 手机端默认不启用 后台下载
downAutoClose: true,
downStream: false,
aria2Rpc: "http://localhost:6800/jsonrpc",
enableAria2Rpc: false,
enableAria2RpcReferer: true,
aria2RpcToken: "",
m3u8AutoDown: true,
badgeNumber: true,
send2local: false,
send2localManual: false,
send2localURL: "http://127.0.0.1:8000/",
send2localMethod: 'POST',
send2localBody: '{"action": "${action}", "data": ${data}, "tabId": "${tabId}"}',
send2localType: 0,
popup: false,
popupMode: 0, // 0:preview.html 1:popup.html 2:window preview.html 3: window popup.html
invoke: false,
invokeText: `m3u8dlre:"\${url}" --save-dir "%USERPROFILE%\\Downloads" --del-after-done --save-name "\${title}_\${now}" --auto-select \${referer|exists:'-H "Referer: *"'}`,
invokeConfirm: false,
// m3u8解析器默认参数
M3u8Thread: 6,
M3u8Mp4: false,
M3u8OnlyAudio: false,
M3u8SkipDecrypt: false,
M3u8StreamSaver: false,
M3u8Ffmpeg: true,
M3u8AutoClose: false,
// 第三方服务地址
onlineServiceAddress: 0,
chromeLimitSize: 1.8 * 1024 * 1024 * 1024,
blockUrl: [],
blockUrlWhite: false,
maxLength: G.isMobile ? 999 : 9999,
sidePanel: false, // 侧边栏
deepSearch: false, // 常开深度搜索
// MQTT 配置
send2MQTT: false,
mqttEnable: false,
mqttBroker: "test.mosquitto.org",
mqttPort: 8081,
mqttPath: "/mqtt",
mqttProtocol: "wss",
mqttClientId: "cat-catch-client",
mqttUser: "",
mqttPassword: "",
mqttTopic: "cat-catch/media",
mqttQos: 0,
mqttTitleLength: 100,
mqttDataFormat: "",
getHtmlDOM: false,
};
// 本地储存的配置
G.LocalVar = {
featMobileTabId: [],
featAutoDownTabId: [],
mediaControl: { tabid: 0, index: -1 },
previewShowTitle: false, // 是否显示标题
previewDeleteDuplicateFilenames: false, // 是否删除重复文件名
};
// 102版本以上 非Firefox 开启更多功能
G.isFirefox = (typeof browser == "object");
G.version = navigator.userAgent.match(/(Chrome|Firefox)\/([\d]+)/);
G.version = G.version && G.version[2] ? parseInt(G.version[2]) : 93;
// 脚本列表
G.scriptList = new Map();
G.scriptList.set("search.js", { key: "search", refresh: true, allFrames: true, world: "MAIN", name: i18n.deepSearch, off: i18n.closeSearch, i18n: false, tabId: new Set() });
G.scriptList.set("catch.js", { key: "catch", refresh: true, allFrames: true, world: "MAIN", name: i18n.cacheCapture, off: i18n.closeCapture, i18n: true, tabId: new Set() });
G.scriptList.set("recorder.js", { key: "recorder", refresh: false, allFrames: true, world: "MAIN", name: i18n.videoRecording, off: i18n.closeRecording, i18n: true, tabId: new Set() });
G.scriptList.set("recorder2.js", { key: "recorder2", refresh: false, allFrames: false, world: "ISOLATED", name: i18n.screenCapture, off: i18n.closeCapture, i18n: true, tabId: new Set() });
G.scriptList.set("webrtc.js", { key: "webrtc", refresh: true, allFrames: true, world: "MAIN", name: i18n.recordWebRTC, off: i18n.closeRecording, i18n: true, tabId: new Set() });
// ffmpeg
G.ffmpegConfig = {
tab: 0,
cacheData: [],
version: 1,
get url() {
return G.onlineServiceAddress == 0 ? "https://ffmpeg.bmmmd.com/" : "https://ffmpeg2.bmmmd.com/";
}
}
// streamSaver 边下边存
G.streamSaverConfig = {
get url() {
return G.onlineServiceAddress == 0 ? "https://stream.bmmmd.com/mitm.html" : "https://stream2.bmmmd.com/mitm.html";
}
}
// 正则预编译
const reFilename = /filename="?([^"]+)"?/;
const reStringModify = /[<>:"\/\\|?*~]/g;
const reFilterFileName = /[<>:"|?*~]/g;
const reTemplates = /\${([^}|]+)(?:\|([^}]+))?}/g;
const reJSONparse = /([{,]\s*)([\w-]+)(\s*:)/g;
// 防抖
let debounce = undefined;
let debounceCount = 0;
let debounceTime = 0;
// Init
InitOptions();
// 初始变量
function InitOptions() {
// 断开重新连接后 立刻把local里MediaData数据交给cacheData
(chrome.storage.session ?? chrome.storage.local).get({ MediaData: {} }, function (items) {
if (items.MediaData.init) {
cacheData = {};
return;
}
cacheData = items.MediaData;
});
// 读取sync配置数据 交给全局变量G
chrome.storage.sync.get(G.OptionLists, function (items) {
if (chrome.runtime.lastError) {
items = G.OptionLists;
}
// 确保有默认值
for (let key in G.OptionLists) {
if (items[key] === undefined || items[key] === null) {
items[key] = G.OptionLists[key];
}
}
// Ext的Array转为Map类型
items.Ext = new Map(items.Ext.map(item => [item.ext, item]));
// Type的Array转为Map类型
items.Type = new Map(items.Type.map(item => [item.type, { size: item.size, state: item.state }]));
// 预编译正则匹配
items.Regex = items.Regex.map(item => {
let reg = undefined;
try { reg = new RegExp(item.regex, item.type) } catch (e) { item.state = false; }
return { regex: reg, ext: item.ext, blackList: item.blackList, state: item.state }
});
// 预编译屏蔽通配符
items.blockUrl = items.blockUrl.map(item => {
return { url: wildcardToRegex(item.url), state: item.state }
});
// 兼容旧配置
if (items.copyM3U8.includes('$url$')) {
items.copyM3U8 = items.copyM3U8.replaceAll('$url$', '${url}').replaceAll('$referer$', '${referer}').replaceAll('$title$', '${title}');
chrome.storage.sync.set({ copyM3U8: items.copyM3U8 });
}
if (items.copyMPD.includes('$url$')) {
items.copyMPD = items.copyMPD.replaceAll('$url$', '${url}').replaceAll('$referer$', '${referer}').replaceAll('$title$', '${title}');
chrome.storage.sync.set({ copyMPD: items.copyMPD });
}
if (items.copyOther.includes('$url$')) {
items.copyOther = items.copyOther.replaceAll('$url$', '${url}').replaceAll('$referer$', '${referer}').replaceAll('$title$', '${title}');
chrome.storage.sync.set({ copyOther: items.copyOther });
}
if (typeof items.m3u8dl == 'boolean') {
items.m3u8dl = items.m3u8dl ? 1 : 0;
chrome.storage.sync.set({ m3u8dl: items.m3u8dl });
}
// 侧边栏
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: items.sidePanel });
G = { ...items, ...G };
// 初始化 G.blockUrlSet
(typeof isLockUrl == 'function') && chrome.tabs.query({}, function (tabs) {
for (const tab of tabs) {
if (tab.url && isLockUrl(tab.url)) {
G.blockUrlSet.add(tab.id);
}
}
});
chrome.action.setIcon({ path: G.enable ? "/img/icon.png" : "/img/icon-disable.png" });
G.initSyncComplete = true;
});
// 读取local配置数据 交给全局变量G
(chrome.storage.session ?? chrome.storage.local).get(G.LocalVar, function (items) {
items.featMobileTabId = new Set(items.featMobileTabId);
items.featAutoDownTabId = new Set(items.featAutoDownTabId);
G = { ...items, ...G };
G.initLocalComplete = true;
});
}
// 监听变化,新值给全局变量
chrome.storage.onChanged.addListener(function (changes, namespace) {
if (changes.MediaData) {
if (changes.MediaData.newValue?.init) { cacheData = {}; }
return;
}
for (let [key, { oldValue, newValue }] of Object.entries(changes)) {
newValue ??= G.OptionLists[key];
if (key == "Ext") {
G.Ext = new Map(newValue.map(item => [item.ext, item]));
continue;
}
if (key == "Type") {
G.Type = new Map(newValue.map(item => [item.type, { size: item.size, state: item.state }]));
continue;
}
if (key == "Regex") {
G.Regex = newValue.map(item => {
let reg = undefined;
try { reg = new RegExp(item.regex, item.type) } catch (e) { item.state = false; }
return { regex: reg, ext: item.ext, blackList: item.blackList, state: item.state }
});
continue;
}
if (key == "blockUrl") {
G.blockUrl = newValue.map(item => {
return { url: wildcardToRegex(item.url), state: item.state }
});
continue;
}
if (key == "featMobileTabId" || key == "featAutoDownTabId") {
G[key] = new Set(newValue);
continue;
}
if (key == "sidePanel" && !G.isFirefox) {
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: newValue });
continue;
}
G[key] = newValue;
}
});
// 扩展升级,清空本地储存
chrome.runtime.onInstalled.addListener(function (details) {
if (details.reason == "update") {
chrome.storage.local.clear(function () {
if (chrome.storage.session) {
chrome.storage.session.clear(InitOptions);
} else {
InitOptions();
}
});
chrome.alarms.create("nowClear", { when: Date.now() + 3000 });
}
if (details.reason == "install") {
chrome.tabs.create({ url: "install.html" });
}
});
/**
* 将用户输入的URL可能包含通配符转换为正则表达式
* @param {string} urlPattern - 用户输入的URL可能包含通配符
* @returns {RegExp} - 转换后的正则表达式
*/
function wildcardToRegex(urlPattern) {
// 将通配符 * 转换为正则表达式的 .*
// 将通配符 ? 转换为正则表达式的 .
// 同时转义其他正则表达式特殊字符
const regexPattern = urlPattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // 转义正则表达式特殊字符
.replace(/\*/g, '.*') // 将 * 替换为 .*
.replace(/\?/g, '.'); // 将 ? 替换为 .
// 创建正则表达式确保匹配整个URL
return new RegExp(`^${regexPattern}$`, 'i'); // 忽略大小写
}