// 低版本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'); // 忽略大小写 }