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

869
js/background.js Normal file
View File

@@ -0,0 +1,869 @@
importScripts("/js/function.js", "/js/init.js");
// Service Worker 5分钟后会强制终止扩展
// https://bugs.chromium.org/p/chromium/issues/detail?id=1271154
// https://stackoverflow.com/questions/66618136/persistent-service-worker-in-chrome-extension/70003493#70003493
chrome.webNavigation.onBeforeNavigate.addListener(function () { return; });
chrome.webNavigation.onHistoryStateUpdated.addListener(function () { return; });
chrome.runtime.onConnect.addListener(function (Port) {
if (chrome.runtime.lastError || Port.name !== "HeartBeat") return;
Port.postMessage("HeartBeat");
Port.onMessage.addListener(function (message, Port) { return; });
const interval = setInterval(function () {
clearInterval(interval);
Port.disconnect();
}, 250000);
Port.onDisconnect.addListener(function () {
interval && clearInterval(interval);
if (chrome.runtime.lastError) { return; }
});
});
/**
* 定时任务
* nowClear clear 清理冗余数据
* save 保存数据
*/
chrome.alarms.onAlarm.addListener(function (alarm) {
if (alarm.name === "nowClear" || alarm.name === "clear") {
clearRedundant();
return;
}
if (alarm.name === "save") {
(chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });
return;
}
});
// onBeforeRequest 浏览器发送请求之前使用正则匹配发送请求的URL
// chrome.webRequest.onBeforeRequest.addListener(
// function (data) {
// try { findMedia(data, true); } catch (e) { console.log(e); }
// }, { urls: ["<all_urls>"] }, ["requestBody"]
// );
// 保存requestHeaders
chrome.webRequest.onSendHeaders.addListener(
function (data) {
if (G && G.initSyncComplete && !G.enable) { return; }
if (data.requestHeaders) {
G.requestHeaders.set(data.requestId, data.requestHeaders);
data.allRequestHeaders = data.requestHeaders;
}
try { findMedia(data, true); } catch (e) { console.log(e); }
}, { urls: ["<all_urls>"] }, ['requestHeaders',
chrome.webRequest.OnBeforeSendHeadersOptions.EXTRA_HEADERS].filter(Boolean)
);
// onResponseStarted 浏览器接收到第一个字节触发,保证有更多信息判断资源类型
chrome.webRequest.onResponseStarted.addListener(
function (data) {
try {
data.allRequestHeaders = G.requestHeaders.get(data.requestId);
if (data.allRequestHeaders) {
G.requestHeaders.delete(data.requestId);
}
findMedia(data);
} catch (e) { console.log(e, data); }
}, { urls: ["<all_urls>"] }, ["responseHeaders"]
);
// 删除失败的requestHeadersData
chrome.webRequest.onErrorOccurred.addListener(
function (data) {
G.requestHeaders.delete(data.requestId);
G.blackList.delete(data.requestId);
}, { urls: ["<all_urls>"] }
);
function findMedia(data, isRegex = false, filter = false, timer = false) {
// Service Worker被强行杀死之后重新自我唤醒等待全局变量初始化完成。
if (!G || !G.initSyncComplete || !G.initLocalComplete || G.tabId == undefined || cacheData.init) {
if (timer) { return; }
setTimeout(() => {
findMedia(data, isRegex, filter, true);
}, 500);
return;
}
// 检查 是否启用 是否在当前标签是否在屏蔽列表中
const blockUrlFlag = data.tabId && data.tabId > 0 && G.blockUrlSet.has(data.tabId);
if (!G.enable || (G.blockUrlWhite ? !blockUrlFlag : blockUrlFlag)) {
return;
}
data.getTime = Date.now();
if (!isRegex && G.blackList.has(data.requestId)) {
G.blackList.delete(data.requestId);
return;
}
// 屏蔽特殊页面发起的资源
if (data.initiator != "null" &&
data.initiator != undefined &&
isSpecialPage(data.initiator)) { return; }
if (G.isFirefox &&
data.originUrl &&
isSpecialPage(data.originUrl)) { return; }
// 屏蔽特殊页面的资源
if (isSpecialPage(data.url)) { return; }
const urlParsing = new URL(data.url);
let [name, ext] = fileNameParse(urlParsing.pathname);
//正则匹配
if (isRegex && !filter) {
for (let key in G.Regex) {
if (!G.Regex[key].state) { continue; }
G.Regex[key].regex.lastIndex = 0;
let result = G.Regex[key].regex.exec(data.url);
if (result == null) { continue; }
if (G.Regex[key].blackList) {
G.blackList.add(data.requestId);
return;
}
data.extraExt = G.Regex[key].ext ? G.Regex[key].ext : undefined;
if (result.length == 1) {
findMedia(data, true, true);
return;
}
result.shift();
result = result.map(str => decodeURIComponent(str));
if (!result[0].startsWith('https://') && !result[0].startsWith('http://')) {
result[0] = urlParsing.protocol + "//" + data.url;
}
data.url = result.join("");
findMedia(data, true, true);
return;
}
return;
}
// 非正则匹配
if (!isRegex) {
// 获取头部信息
data.header = getResponseHeadersValue(data);
//检查后缀
if (!filter && ext != undefined) {
filter = CheckExtension(ext, data.header?.size);
if (filter == "break") { return; }
}
//检查类型
if (!filter && data.header?.type != undefined) {
filter = CheckType(data.header.type, data.header?.size);
if (filter == "break") { return; }
}
//查找附件
if (!filter && data.header?.attachment != undefined) {
const res = data.header.attachment.match(reFilename);
if (res && res[1]) {
[name, ext] = fileNameParse(decodeURIComponent(res[1]));
filter = CheckExtension(ext, 0);
if (filter == "break") { return; }
}
}
//放过类型为media的资源
if (data.type == "media") {
filter = true;
}
}
if (!filter) { return; }
// 谜之原因 获取得资源 tabId可能为 -1 firefox中则正常
// 检查是 -1 使用当前激活标签得tabID
data.tabId = data.tabId == -1 ? G.tabId : data.tabId;
cacheData[data.tabId] ??= [];
cacheData[G.tabId] ??= [];
// 缓存数据大于9999条 清空缓存 避免内存占用过多
if (cacheData[data.tabId].length > G.maxLength) {
cacheData[data.tabId] = [];
(chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });
return;
}
// 查重 避免CPU占用 大于500 强制关闭查重
// if (G.checkDuplicates && cacheData[data.tabId].length <= 500) {
// for (let item of cacheData[data.tabId]) {
// if (item.url.length == data.url.length &&
// item.cacheURL.pathname == urlParsing.pathname &&
// item.cacheURL.host == urlParsing.host &&
// item.cacheURL.search == urlParsing.search) { return; }
// }
// }
if (G.checkDuplicates && cacheData[data.tabId].length <= 500) {
const tabFingerprints = G.urlMap.get(data.tabId) || new Set();
if (tabFingerprints.has(data.url)) {
return; // 找到重复,直接返回
}
tabFingerprints.add(data.url);
G.urlMap.set(data.tabId, tabFingerprints);
if (tabFingerprints.size >= 500) {
tabFingerprints.clear();
}
}
chrome.tabs.get(data.tabId, async function (webInfo) {
if (chrome.runtime.lastError) { return; }
data.requestHeaders = getRequestHeaders(data);
// requestHeaders 中cookie 单独列出来
if (data.requestHeaders?.cookie) {
data.cookie = data.requestHeaders.cookie;
data.requestHeaders.cookie = undefined;
}
const info = {
name: name,
url: data.url,
size: data.header?.size,
ext: ext,
type: data.mime ?? data.header?.type,
tabId: data.tabId,
isRegex: isRegex,
requestId: data.requestId ?? Date.now().toString(),
initiator: data.initiator,
requestHeaders: data.requestHeaders,
cookie: data.cookie,
// cacheURL: { host: urlParsing.host, search: urlParsing.search, pathname: urlParsing.pathname },
getTime: data.getTime
};
// 不存在扩展使用类型
if (info.ext === undefined && info.type !== undefined) {
info.ext = info.type.split("/")[1];
}
// 正则匹配的备注扩展
if (data.extraExt) {
info.ext = data.extraExt;
}
// 不存在 initiator 和 referer 使用web url代替initiator
if (info.initiator == undefined || info.initiator == "null") {
info.initiator = info.requestHeaders?.referer ?? webInfo?.url;
}
// 装载页面信息
info.title = webInfo?.title ?? "NULL";
info.favIconUrl = webInfo?.favIconUrl;
info.webUrl = webInfo?.url;
// 屏蔽资源
if (!isRegex && G.blackList.has(data.requestId)) {
G.blackList.delete(data.requestId);
return;
}
// 发送到popup 并检查自动下载
chrome.runtime.sendMessage({ Message: "popupAddData", data: info }, function () {
if (G.featAutoDownTabId.size > 0 && G.featAutoDownTabId.has(info.tabId) && chrome.downloads?.State) {
try {
const downDir = info.title == "NULL" ? "CatCatch/" : stringModify(info.title) + "/";
let fileName = isEmpty(info.name) ? stringModify(info.title) + '.' + info.ext : decodeURIComponent(stringModify(info.name));
if (G.TitleName) {
fileName = filterFileName(templates(G.downFileName, info));
} else {
fileName = downDir + fileName;
}
chrome.downloads.download({
url: info.url,
filename: fileName
});
} catch (e) { return; }
}
if (chrome.runtime.lastError) { return; }
});
// 数据发送
if (G.send2local) {
try { send2local("catch", { ...info, requestHeaders: data.allRequestHeaders }, info.tabId); } catch (e) { console.log(e); }
}
// 储存数据
cacheData[info.tabId] ??= [];
cacheData[info.tabId].push(info);
// 当前标签媒体数量大于100 开启防抖 等待5秒储存 或 积累10个资源储存一次。
if (cacheData[info.tabId].length >= 100 && debounceCount <= 10) {
debounceCount++;
clearTimeout(debounce);
debounce = setTimeout(function () { save(info.tabId); }, 5000);
return;
}
// 时间间隔小于500毫秒 等待2秒储存
if (Date.now() - debounceTime <= 500) {
clearTimeout(debounce);
debounceTime = Date.now();
debounce = setTimeout(function () { save(info.tabId); }, 2000);
return;
}
save(info.tabId);
});
}
// cacheData数据 储存到 chrome.storage.local
function save(tabId) {
clearTimeout(debounce);
debounceTime = Date.now();
debounceCount = 0;
(chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData }, function () {
chrome.runtime.lastError && console.log(chrome.runtime.lastError);
});
cacheData[tabId] && SetIcon({ number: cacheData[tabId].length, tabId: tabId });
}
/**
* 监听 扩展 message 事件
*/
chrome.runtime.onMessage.addListener(function (Message, sender, sendResponse) {
if (chrome.runtime.lastError) { return; }
if (!G.initLocalComplete || !G.initSyncComplete) {
sendResponse("error");
return true;
}
// 以下检查是否有 tabId 不存在使用当前标签
Message.tabId = Message.tabId ?? G.tabId;
// 从缓存中保存数据到本地
if (Message.Message == "pushData") {
(chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });
sendResponse("ok");
return true;
}
// 获取所有数据
if (Message.Message == "getAllData") {
sendResponse(cacheData);
return true;
}
/**
* 设置扩展图标数字
* 提供 type 删除标签为 tabId 的数字
* 不提供type 删除所有标签的数字
*/
if (Message.Message == "ClearIcon") {
Message.type ? SetIcon({ tabId: Message.tabId }) : SetIcon();
sendResponse("ok");
return true;
}
// 启用/禁用扩展
if (Message.Message == "enable") {
G.enable = !G.enable;
chrome.storage.sync.set({ enable: G.enable });
chrome.action.setIcon({ path: G.enable ? "/img/icon.png" : "/img/icon-disable.png" });
sendResponse(G.enable);
return true;
}
/**
* 提供requestId数组 获取指定的数据
*/
if (Message.Message == "getData" && Message.requestId) {
// 判断Message.requestId是否数组
if (!Array.isArray(Message.requestId)) {
Message.requestId = [Message.requestId];
}
const response = [];
if (Message.requestId.length) {
for (let item in cacheData) {
for (let data of cacheData[item]) {
if (Message.requestId.includes(data.requestId)) {
response.push(data);
}
}
}
}
sendResponse(response.length ? response : "error");
return true;
}
/**
* 提供 tabId 获取该标签数据
*/
if (Message.Message == "getData") {
sendResponse(cacheData[Message.tabId]);
return true;
}
/**
* 获取各按钮状态
* 模拟手机 自动下载 启用 以及各种脚本状态
*/
if (Message.Message == "getButtonState") {
let state = {
MobileUserAgent: G.featMobileTabId.has(Message.tabId),
AutoDown: G.featAutoDownTabId.has(Message.tabId),
enable: G.enable,
}
G.scriptList.forEach(function (item, key) {
state[item.key] = item.tabId.has(Message.tabId);
});
sendResponse(state);
return true;
}
// 对tabId的标签 进行模拟手机操作
if (Message.Message == "mobileUserAgent") {
mobileUserAgent(Message.tabId, !G.featMobileTabId.has(Message.tabId));
chrome.tabs.reload(Message.tabId, { bypassCache: true });
sendResponse("ok");
return true;
}
// 对tabId的标签 开启 关闭 自动下载
if (Message.Message == "autoDown") {
if (G.featAutoDownTabId.has(Message.tabId)) {
G.featAutoDownTabId.delete(Message.tabId);
} else {
G.featAutoDownTabId.add(Message.tabId);
}
(chrome.storage.session ?? chrome.storage.local).set({ featAutoDownTabId: Array.from(G.featAutoDownTabId) });
sendResponse("ok");
return true;
}
// 对tabId的标签 脚本注入或删除
if (Message.Message == "script") {
if (!G.scriptList.has(Message.script)) {
sendResponse("error no exists");
return false;
}
const script = G.scriptList.get(Message.script);
const scriptTabid = script.tabId;
const refresh = Message.refresh ?? script.refresh;
if (scriptTabid.has(Message.tabId)) {
scriptTabid.delete(Message.tabId);
if (Message.script == "search.js") {
G.deepSearchTemporarilyClose = Message.tabId;
}
refresh && chrome.tabs.reload(Message.tabId, { bypassCache: true });
sendResponse("ok");
return true;
}
scriptTabid.add(Message.tabId);
if (refresh) {
chrome.tabs.reload(Message.tabId, { bypassCache: true });
} else {
const files = [`catch-script/${Message.script}`];
script.i18n && files.unshift("catch-script/i18n.js");
chrome.scripting.executeScript({
target: { tabId: Message.tabId, allFrames: script.allFrames },
files: files,
injectImmediately: true,
world: script.world
});
}
sendResponse("ok");
return true;
}
// 脚本注入 脚本申请多语言文件
if (Message.Message == "scriptI18n") {
chrome.scripting.executeScript({
target: { tabId: Message.tabId, allFrames: true },
files: ["catch-script/i18n.js"],
injectImmediately: true,
world: "MAIN"
});
sendResponse("ok");
return true;
}
// Heart Beat
if (Message.Message == "HeartBeat") {
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
if (tabs[0] && tabs[0].id) {
G.tabId = tabs[0].id;
}
});
sendResponse("HeartBeat OK");
return true;
}
// 清理数据
if (Message.Message == "clearData") {
// 当前标签
if (Message.type) {
delete cacheData[Message.tabId];
(chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });
clearRedundant();
sendResponse("OK");
return true;
}
// 其他标签
for (let item in cacheData) {
if (item == Message.tabId) { continue; }
delete cacheData[item];
}
(chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });
clearRedundant();
sendResponse("OK");
return true;
}
// 清理冗余数据
if (Message.Message == "clearRedundant") {
clearRedundant();
sendResponse("OK");
return true;
}
// 从 content-script 或 catch-script 传来的媒体url
if (Message.Message == "addMedia") {
chrome.tabs.query({}, function (tabs) {
for (let item of tabs) {
if (item.url == Message.href) {
findMedia({ url: Message.url, tabId: item.id, extraExt: Message.extraExt, mime: Message.mime, requestId: Message.requestId, requestHeaders: Message.requestHeaders }, true, true);
return true;
}
}
findMedia({ url: Message.url, tabId: -1, extraExt: Message.extraExt, mime: Message.mime, requestId: Message.requestId, initiator: Message.href, requestHeaders: Message.requestHeaders }, true, true);
});
sendResponse("ok");
return true;
}
// ffmpeg网页通信
if (Message.Message == "catCatchFFmpeg") {
const data = { ...Message, Message: "ffmpeg", tabId: Message.tabId ?? sender.tab.id, version: G.ffmpegConfig.version };
chrome.tabs.query({ url: G.ffmpegConfig.url + "*" }, function (tabs) {
if (chrome.runtime.lastError || !tabs.length) {
chrome.tabs.create({ url: G.ffmpegConfig.url, active: Message.active ?? true }, function (tab) {
if (chrome.runtime.lastError) { return; }
G.ffmpegConfig.tab = tab.id;
G.ffmpegConfig.cacheData.push(data);
});
return true;
}
if (tabs[0].status == "complete") {
chrome.tabs.sendMessage(tabs[0].id, data);
} else {
G.ffmpegConfig.tab = tabs[0].id;
G.ffmpegConfig.cacheData.push(data);
}
});
sendResponse("ok");
return true;
}
// 发送数据到本地
if (Message.Message == "send2local" && G.send2local) {
try { send2local(Message.action, Message.data, Message.tabId); } catch (e) { console.log(e); }
sendResponse("ok");
return true;
}
});
// 选定标签 更新G.tabId
// chrome.tabs.onHighlighted.addListener(function (activeInfo) {
// if (activeInfo.windowId == -1 || !activeInfo.tabIds || !activeInfo.tabIds.length) { return; }
// G.tabId = activeInfo.tabIds[0];
// });
/**
* 监听 切换标签
* 更新全局变量 G.tabId 为当前标签
*/
chrome.tabs.onActivated.addListener(function (activeInfo) {
G.tabId = activeInfo.tabId;
if (cacheData[G.tabId] !== undefined) {
SetIcon({ number: cacheData[G.tabId].length, tabId: G.tabId });
return;
}
SetIcon({ tabId: G.tabId });
});
// 切换窗口更新全局变量G.tabId
chrome.windows.onFocusChanged.addListener(function (activeInfo) {
if (activeInfo == -1) { return; }
chrome.tabs.query({ active: true, windowId: activeInfo }, function (tabs) {
if (tabs[0] && tabs[0].id) {
G.tabId = tabs[0].id;
} else {
G.tabId = -1;
}
});
}, { filters: ["normal"] });
/**
* 监听 标签页面更新
* 检查 清理数据
* 检查 是否在屏蔽列表中
*/
chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
if (isSpecialPage(tab.url) || tabId <= 0 || !G.initSyncComplete) { return; }
if (changeInfo.status && changeInfo.status == "loading" && G.autoClearMode == 2) {
G.urlMap.delete(tabId);
chrome.alarms.get("save", function (alarm) {
if (!alarm) {
delete cacheData[tabId];
SetIcon({ tabId: tabId });
chrome.alarms.create("save", { when: Date.now() + 1000 });
}
});
}
// 检查当前标签是否在屏蔽列表中
if (changeInfo.url && tabId > 0 && G.blockUrl.length) {
G.blockUrlSet.delete(tabId);
if (isLockUrl(changeInfo.url)) {
G.blockUrlSet.add(tabId);
}
}
chrome.sidePanel.setOptions({
tabId,
path: "popup.html?tabId=" + tabId
});
});
/**
* 监听 frame 正在载入
* 检查 是否在屏蔽列表中 (frameId == 0 为主框架)
* 检查 自动清理 (frameId == 0 为主框架)
* 检查 注入脚本
*/
chrome.webNavigation.onCommitted.addListener(function (details) {
if (isSpecialPage(details.url) || details.tabId <= 0 || !G.initSyncComplete) { return; }
// 刷新页面 检查是否在屏蔽列表中
if (details.frameId == 0 && details.transitionType == "reload") {
G.blockUrlSet.delete(details.tabId);
if (isLockUrl(details.url)) {
G.blockUrlSet.add(details.tabId);
}
}
// 刷新清理角标数
if (details.frameId == 0 && (!['auto_subframe', 'manual_subframe', 'form_submit'].includes(details.transitionType)) && G.autoClearMode == 1) {
delete cacheData[details.tabId];
G.urlMap.delete(details.tabId);
(chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });
SetIcon({ tabId: details.tabId });
}
// chrome内核版本 102 以下不支持 chrome.scripting.executeScript API
if (G.version < 102) { return; }
if (G.deepSearch && G.deepSearchTemporarilyClose != details.tabId) {
G.scriptList.get("search.js").tabId.add(details.tabId);
G.deepSearchTemporarilyClose = null;
}
// catch-script 脚本
G.scriptList.forEach(function (item, script) {
if (!item.tabId.has(details.tabId) || !item.allFrames) { return true; }
const files = [`catch-script/${script}`];
item.i18n && files.unshift("catch-script/i18n.js");
chrome.scripting.executeScript({
target: { tabId: details.tabId, frameIds: [details.frameId] },
files: files,
injectImmediately: true,
world: item.world
});
});
// 模拟手机
if (G.initLocalComplete && G.featMobileTabId.size > 0 && G.featMobileTabId.has(details.tabId)) {
chrome.scripting.executeScript({
args: [G.MobileUserAgent.toString()],
target: { tabId: details.tabId, frameIds: [details.frameId] },
func: function () {
Object.defineProperty(navigator, 'userAgent', { value: arguments[0], writable: false });
},
injectImmediately: true,
world: "MAIN"
});
}
});
/**
* 监听 标签关闭 清理数据
*/
chrome.tabs.onRemoved.addListener(function (tabId) {
// 清理缓存数据
chrome.alarms.get("nowClear", function (alarm) {
!alarm && chrome.alarms.create("nowClear", { when: Date.now() + 1000 });
});
if (G.initSyncComplete) {
G.blockUrlSet.has(tabId) && G.blockUrlSet.delete(tabId);
}
});
/**
* 浏览器 扩展快捷键
*/
chrome.commands.onCommand.addListener(function (command) {
if (command == "auto_down") {
if (G.featAutoDownTabId.has(G.tabId)) {
G.featAutoDownTabId.delete(G.tabId);
} else {
G.featAutoDownTabId.add(G.tabId);
}
(chrome.storage.session ?? chrome.storage.local).set({ featAutoDownTabId: Array.from(G.featAutoDownTabId) });
} else if (command == "catch") {
const scriptTabid = G.scriptList.get("catch.js").tabId;
scriptTabid.has(G.tabId) ? scriptTabid.delete(G.tabId) : scriptTabid.add(G.tabId);
chrome.tabs.reload(G.tabId, { bypassCache: true });
} else if (command == "m3u8") {
chrome.tabs.create({ url: "m3u8.html" });
} else if (command == "clear") {
delete cacheData[G.tabId];
(chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });
clearRedundant();
SetIcon({ tabId: G.tabId });
} else if (command == "enable") {
G.enable = !G.enable;
chrome.storage.sync.set({ enable: G.enable });
chrome.action.setIcon({ path: G.enable ? "/img/icon.png" : "/img/icon-disable.png" });
} else if (command == "reboot") {
chrome.runtime.reload();
} else if (command == "deepSearch") {
const script = G.scriptList.get("search.js");
const scriptTabid = script.tabId;
if (scriptTabid.has(G.tabId)) {
scriptTabid.delete(G.tabId);
G.deepSearchTemporarilyClose = G.tabId;
chrome.tabs.reload(G.tabId, { bypassCache: true });
return;
}
scriptTabid.add(G.tabId);
chrome.tabs.reload(G.tabId, { bypassCache: true });
}
});
/**
* 监听 页面完全加载完成 判断是否在线ffmpeg页面
* 如果是在线ffmpeg 则发送数据
*/
chrome.webNavigation.onCompleted.addListener(function (details) {
if (G.ffmpegConfig.tab && details.tabId == G.ffmpegConfig.tab) {
setTimeout(() => {
G.ffmpegConfig.cacheData.forEach(data => {
chrome.tabs.sendMessage(details.tabId, data);
});
G.ffmpegConfig.cacheData = [];
G.ffmpegConfig.tab = 0;
}, 500);
}
});
/**
* 检查扩展名和大小
* @param {String} ext
* @param {Number} size
* @returns {Boolean|String}
*/
function CheckExtension(ext, size) {
const Ext = G.Ext.get(ext);
if (!Ext) { return false; }
if (!Ext.state) { return "break"; }
if (Ext.size != 0 && size != undefined && size <= Ext.size * 1024) { return "break"; }
return true;
}
/**
* 检查类型和大小
* @param {String} dataType
* @param {Number} dataSize
* @returns {Boolean|String}
*/
function CheckType(dataType, dataSize) {
const typeInfo = G.Type.get(dataType.split("/")[0] + "/*") || G.Type.get(dataType);
if (!typeInfo) { return false; }
if (!typeInfo.state) { return "break"; }
if (typeInfo.size != 0 && dataSize != undefined && dataSize <= typeInfo.size * 1024) { return "break"; }
return true;
}
/**
* 获取文件名及扩展名
* @param {String} pathname
* @returns {Array}
*/
function fileNameParse(pathname) {
let fileName = decodeURI(pathname.split("/").pop());
let ext = fileName.split(".");
ext = ext.length == 1 ? undefined : ext.pop().toLowerCase();
return [fileName, ext ? ext : undefined];
}
/**
* 获取响应头信息
* @param {Object} data
* @returns {Object}
*/
function getResponseHeadersValue(data) {
const header = {};
if (data.responseHeaders == undefined || data.responseHeaders.length == 0) { return header; }
for (let item of data.responseHeaders) {
item.name = item.name.toLowerCase();
if (item.name == "content-length") {
header.size ??= parseInt(item.value);
} else if (item.name == "content-type") {
header.type = item.value.split(";")[0].toLowerCase();
} else if (item.name == "content-disposition") {
header.attachment = item.value;
} else if (item.name == "content-range") {
let size = item.value.split('/')[1];
if (size !== '*') {
header.size = parseInt(size);
}
}
}
return header;
}
/**
* 获取请求头
* @param {Object} data
* @returns {Object|Boolean}
*/
function getRequestHeaders(data) {
if (data.allRequestHeaders == undefined || data.allRequestHeaders.length == 0) { return false; }
const header = {};
for (let item of data.allRequestHeaders) {
item.name = item.name.toLowerCase();
if (item.name == "referer") {
header.referer = item.value;
} else if (item.name == "origin") {
header.origin = item.value;
} else if (item.name == "cookie") {
header.cookie = item.value;
} else if (item.name == "authorization") {
header.authorization = item.value;
}
}
if (Object.keys(header).length) {
return header;
}
return false;
}
//设置扩展图标
function SetIcon(obj) {
if (obj?.number == 0 || obj?.number == undefined) {
chrome.action.setBadgeText({ text: "", tabId: obj?.tabId ?? G.tabId }, function () { if (chrome.runtime.lastError) { return; } });
// chrome.action.setTitle({ title: "还没闻到味儿~", tabId: obj.tabId }, function () { if (chrome.runtime.lastError) { return; } });
} else if (G.badgeNumber) {
obj.number = obj.number > 999 ? "999+" : obj.number.toString();
chrome.action.setBadgeText({ text: obj.number, tabId: obj.tabId }, function () { if (chrome.runtime.lastError) { return; } });
// chrome.action.setTitle({ title: "抓到 " + obj.number + " 条鱼", tabId: obj.tabId }, function () { if (chrome.runtime.lastError) { return; } });
}
}
// 模拟手机端
function mobileUserAgent(tabId, change = false) {
if (change) {
G.featMobileTabId.add(tabId);
(chrome.storage.session ?? chrome.storage.local).set({ featMobileTabId: Array.from(G.featMobileTabId) });
chrome.declarativeNetRequest.updateSessionRules({
removeRuleIds: [tabId],
addRules: [{
"id": tabId,
"action": {
"type": "modifyHeaders",
"requestHeaders": [{
"header": "User-Agent",
"operation": "set",
"value": G.MobileUserAgent
}]
},
"condition": {
"tabIds": [tabId],
"resourceTypes": Object.values(chrome.declarativeNetRequest.ResourceType)
}
}]
});
return true;
}
G.featMobileTabId.delete(tabId) && (chrome.storage.session ?? chrome.storage.local).set({ featMobileTabId: Array.from(G.featMobileTabId) });
chrome.declarativeNetRequest.updateSessionRules({
removeRuleIds: [tabId]
});
}
// 判断特殊页面
function isSpecialPage(url) {
if (!url || url == "null") { return true; }
return !(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("blob:"));
}
// 测试
// chrome.storage.local.get(function (data) { console.log(data.MediaData) });
// chrome.declarativeNetRequest.getSessionRules(function (rules) { console.log(rules); });
// chrome.tabs.query({}, function (tabs) { for (let item of tabs) { console.log(item.id); } });

274
js/content-script.js Normal file
View File

@@ -0,0 +1,274 @@
(function () {
var _videoObj = [];
var _videoSrc = [];
var _key = new Set();
chrome.runtime.onMessage.addListener(function (Message, sender, sendResponse) {
if (chrome.runtime.lastError) { return; }
// 获取页面视频对象
if (Message.Message == "getVideoState") {
let videoObj = [];
let videoSrc = [];
document.querySelectorAll("video, audio").forEach(function (video) {
if (video.currentSrc != "" && video.currentSrc != undefined) {
videoObj.push(video);
videoSrc.push(video.currentSrc);
}
});
const iframe = document.querySelectorAll("iframe");
if (iframe.length > 0) {
iframe.forEach(function (iframe) {
if (iframe.contentDocument == null) { return true; }
iframe.contentDocument.querySelectorAll("video, audio").forEach(function (video) {
if (video.currentSrc != "" && video.currentSrc != undefined) {
videoObj.push(video);
videoSrc.push(video.currentSrc);
}
});
});
}
if (videoObj.length > 0) {
if (videoObj.length !== _videoObj.length || videoSrc.toString() !== _videoSrc.toString()) {
_videoSrc = videoSrc;
_videoObj = videoObj;
}
Message.index = Message.index == -1 ? 0 : Message.index;
const video = videoObj[Message.index];
const timePCT = video.currentTime / video.duration * 100;
sendResponse({
time: timePCT,
currentTime: video.currentTime,
duration: video.duration,
volume: video.volume,
count: _videoObj.length,
src: _videoSrc,
paused: video.paused,
loop: video.loop,
speed: video.playbackRate,
muted: video.muted,
type: video.tagName.toLowerCase()
});
return true;
}
sendResponse({ count: 0 });
return true;
}
// 速度控制
if (Message.Message == "speed") {
_videoObj[Message.index].playbackRate = Message.speed;
return true;
}
// 画中画
if (Message.Message == "pip") {
if (document.pictureInPictureElement) {
try { document.exitPictureInPicture(); } catch (e) { return true; }
sendResponse({ state: false });
return true;
}
try { _videoObj[Message.index].requestPictureInPicture(); } catch (e) { return true; }
sendResponse({ state: true });
return true;
}
// 全屏
if (Message.Message == "fullScreen") {
if (document.fullscreenElement) {
try { document.exitFullscreen(); } catch (e) { return true; }
sendResponse({ state: false });
return true;
}
setTimeout(function () {
try { _videoObj[Message.index].requestFullscreen(); } catch (e) { return true; }
}, 500);
sendResponse({ state: true });
return true;
}
// 播放
if (Message.Message == "play") {
_videoObj[Message.index].play();
return true;
}
// 暂停
if (Message.Message == "pause") {
_videoObj[Message.index].pause();
return true;
}
// 循环播放
if (Message.Message == "loop") {
_videoObj[Message.index].loop = Message.action;
return true;
}
// 设置音量
if (Message.Message == "setVolume") {
_videoObj[Message.index].volume = Message.volume;
sendResponse("ok");
return true;
}
// 静音
if (Message.Message == "muted") {
_videoObj[Message.index].muted = Message.action;
return true;
}
// 设置视频进度
if (Message.Message == "setTime") {
const time = Message.time * _videoObj[Message.index].duration / 100;
_videoObj[Message.index].currentTime = time;
sendResponse("ok");
return true;
}
// 截图视频图片
if (Message.Message == "screenshot") {
try {
let video = _videoObj[Message.index];
let canvas = document.createElement("canvas");
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext("2d").drawImage(video, 0, 0, canvas.width, canvas.height);
let link = document.createElement("a");
link.href = canvas.toDataURL("image/jpeg");
link.download = `${location.hostname}-${secToTime(video.currentTime)}.jpg`;
link.click();
canvas = null;
link = null;
sendResponse("ok");
return true;
} catch (e) { console.log(e); return true; }
}
if (Message.Message == "getKey") {
sendResponse(Array.from(_key));
return true;
}
if (Message.Message == "ffmpeg") {
if (!Message.files) {
window.postMessage(Message);
sendResponse("ok");
return true;
}
Message.quantity ??= Message.files.length;
for (let item of Message.files) {
const data = { ...Message, ...item };
data.type = item.type ?? "video";
if (data.data instanceof Blob) {
window.postMessage(data);
} else {
fetch(data.data)
.then(response => response.blob())
.then(blob => {
data.data = blob;
window.postMessage(data);
});
}
}
sendResponse("ok");
return true;
}
if (Message.Message == "getPage") {
if (Message.find) {
const DOM = document.querySelector(Message.find);
DOM ? sendResponse(DOM.innerHTML) : sendResponse("");
return true;
}
sendResponse(document.documentElement.outerHTML);
return true;
}
});
// Heart Beat
var Port;
function connect() {
Port = chrome.runtime.connect(chrome.runtime.id, { name: "HeartBeat" });
Port.postMessage("HeartBeat");
Port.onMessage.addListener(function (message, Port) { return true; });
Port.onDisconnect.addListener(connect);
}
connect();
function secToTime(sec) {
let time = "";
let hour = Math.floor(sec / 3600);
let min = Math.floor((sec % 3600) / 60);
sec = Math.floor(sec % 60);
if (hour > 0) { time = hour + "'"; }
if (min < 10) { time += "0"; }
time += min + "'";
if (sec < 10) { time += "0"; }
time += sec;
return time;
}
window.addEventListener("message", (event) => {
if (!event.data || !event.data.action) { return; }
if (event.data.action == "catCatchAddMedia") {
if (!event.data.url) { return; }
chrome.runtime.sendMessage({
Message: "addMedia",
url: event.data.url,
href: event.data.href ?? event.source.location.href,
extraExt: event.data.ext,
mime: event.data.mime,
requestHeaders: { referer: event.data.referer },
requestId: event.data.requestId
});
}
if (event.data.action == "catCatchAddKey") {
let key = event.data.key;
if (key instanceof ArrayBuffer || key instanceof Array) {
key = ArrayToBase64(key);
}
if (_key.has(key)) { return; }
_key.add(key);
chrome.runtime.sendMessage({
Message: "send2local",
action: "addKey",
data: key,
});
chrome.runtime.sendMessage({
Message: "popupAddKey",
data: key,
url: event.data.url,
});
}
if (event.data.action == "catCatchFFmpeg") {
if (!event.data.use ||
!event.data.files ||
!event.data.files instanceof Array ||
event.data.files.length == 0
) { return; }
event.data.title = event.data.title ?? document.title ?? new Date().getTime().toString();
event.data.title = event.data.title.replaceAll('"', "").replaceAll("'", "").replaceAll(" ", "");
let data = {
Message: event.data.action,
action: event.data.use,
files: event.data.files,
url: event.data.href ?? event.source.location.href,
};
data = { ...event.data, ...data };
chrome.runtime.sendMessage(data);
}
if (event.data.action == "catCatchFFmpegResult") {
if (!event.data.state || !event.data.tabId) { return; }
chrome.runtime.sendMessage({ Message: "catCatchFFmpegResult", ...event.data });
}
if (event.data.action == "catCatchToBackground") {
delete event.data.action;
chrome.runtime.sendMessage(event.data);
}
if (event.data.action == "catCatchDashDRMMedia") {
// TODO DRM Media
console.log("DRM Media", event);
}
}, false);
function ArrayToBase64(data) {
try {
let bytes = new Uint8Array(data);
let binary = "";
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
if (typeof _btoa == "function") {
return _btoa(binary);
}
return btoa(binary);
} catch (e) {
return false;
}
}
})();

421
js/downloader.js Normal file
View File

@@ -0,0 +1,421 @@
// url 参数解析
const params = new URL(location.href).searchParams;
const _requestId = params.get("requestId") ? params.get("requestId").split(",") : []; // 要下载得资源ID
const _ffmpeg = params.get("ffmpeg"); // 启用在线FFmpeg
let _downStream = params.get("downStream"); // 启用边下边存 流式下载
const _data = []; // 通过_requestId获取得到得数据
const _taskId = Date.parse(new Date()); // 配合ffmpeg使用的任务ID 以便在线ffmpeg通过ID知道文件属于哪些任务
let _tabId = null; // 当前页面tab id
let _index = null; // 当前页面 tab index
// 是否表单提交下载 表单提交 不使用自定义文件名
const downloadData = localStorage.getItem('downloadData') ? JSON.parse(localStorage.getItem('downloadData')) : [];
awaitG(() => {
loadCSS();
// 获取当前标签信息
chrome.tabs.getCurrent(function (tabs) {
_tabId = tabs.id;
_index = tabs.index;
// 如果没有requestId 显示 提交表单
if (!_requestId.length) {
$("#downStream").prop("checked", G.downStream);
$("#getURL, .newDownload").toggle();
$("#getURL_btn").click(function () {
const data = [{
url: $("#getURL #url").val().trim(),
requestId: 1,
}];
// 处理请求头 如果是url直接放入referer 支持json格式
const referer = $("#getURL #referer").val().trim();
if (referer) {
if (referer.startsWith("http")) {
data[0].requestHeaders = { referer: referer };
} else {
data[0].requestHeaders = JSONparse(referer);
}
}
_downStream = $("#downStream").prop("checked");
_data.push(...data);
setHeaders(data, start(), _tabId);
$("#getURL, .newDownload").toggle();
});
return;
}
// 优先从downloadData 提取任务数据
for (let item of downloadData) {
if (_requestId.includes(item.requestId)) {
_data.push(item);
_requestId.splice(_requestId.indexOf(item.requestId), 1);
}
}
if (!_requestId.length) {
setHeaders(_data, start(), _tabId);
return;
}
// downloadData 不存在 从后台获取数据
chrome.runtime.sendMessage({ Message: "getData", requestId: _requestId }, function (data) {
if (data == "error" || !Array.isArray(data) || chrome.runtime.lastError || data.length == 0) {
alert(i18n.dataFetchFailed);
return;
}
_data.push(...data);
setHeaders(data, start(), _tabId);
});
});
});
function start() {
// 提前打开ffmpeg页面
if (_ffmpeg) {
chrome.runtime.sendMessage({
Message: "catCatchFFmpeg",
action: "openFFmpeg",
extra: i18n.waitingForMedia
});
}
$("#autoClose").prop("checked", G.downAutoClose);
streamSaver.mitm = G.streamSaverConfig.url;
const $downBox = $("#downBox"); // 下载列表容器
const down = new Downloader(_data); // 创建下载器
const itemDOM = new Map(); // 提前储存需要平凡操作的dom对象 提高效率
$("#test").click(() => console.log(down));
// 添加html
const addHtml = (fragment) => {
if (!fragment.downFileName) {
fragment.downFileName = getUrlFileName(fragment.url);
}
const html = $(`
<div class="downItem">
<div class="explain">${fragment.downFileName}</div>
<div id="downFilepProgress"></div>
<div class="progress-container">
<div class="progress-wrapper">
<div class="progress-bar">
<div class="progress"></div>
</div>
</div>
<button class="cancel-btn">${i18n.stopDownload}</button>
</div>
</div>`);
const $button = html.find("button");
$button.data("action", "stop");
// 操作对象放入itemDOM 提高效率
itemDOM.set(fragment.index, {
progressText: html.find("#downFilepProgress"),
progress: html.find(".progress"),
button: $button
});
$button.click(function () {
const action = $(this).data("action");
if (action == "stop") {
down.stop(fragment.index);
$(this).html(i18n.retryDownload).data("action", "start");
if (fragment.fileStream) {
fragment.fileStream.close();
}
} else if (action == "start") {
if (fragment.fileStream) {
fragment.fileStream = streamSaver.createWriteStream(fragment.downFileName).getWriter();
}
down.state = "waiting";
down.downloader(fragment);
$(this).html(i18n.stopDownload).data("action", "stop");
}
});
$downBox.append(html);
// 流式下载处理
if ((_downStream || G.downStream) && !_ffmpeg) {
fragment.fileStream = streamSaver.createWriteStream(fragment.downFileName).getWriter();
}
}
// 下载列表添加对应html
down.fragments.forEach(addHtml);
// 文件进程事件
let lastEmitted = Date.now();
down.on('itemProgress', function (fragment, state, receivedLength, contentLength, value) {
// 通过 lastEmitted 限制更新频率 避免疯狂dom操作
if (Date.now() - lastEmitted >= 100 && !state) {
const $dom = itemDOM.get(fragment.index);
if (contentLength) {
const progress = (receivedLength / contentLength * 100).toFixed(2) + "%";
$dom.progress.css("width", progress).html(progress);
$dom.progressText.html(`${byteToSize(receivedLength)} / ${byteToSize(contentLength)}`);
} else {
$dom.progressText.html(`${byteToSize(receivedLength)}`);
}
if (down.total == 1) {
const title = contentLength ?
`${byteToSize(receivedLength)} / ${byteToSize(contentLength)}` :
`${byteToSize(receivedLength)}`;
document.title = title;
}
lastEmitted = Date.now();
}
});
// 单文件下载完成事件
down.on('completed', function (buffer, fragment) {
const $dom = itemDOM.get(fragment.index);
$dom.progress.css("width", "100%").html("100%");
$dom.progressText.html(i18n.downloadComplete);
$dom.button.html(i18n.sendFfmpeg).data("action", "sendFfmpeg");
document.title = `${down.success}/${down.total}`;
$dom.button.hide();
// 是流式下载 停止写入
if (fragment.fileStream) {
fragment.fileStream.close();
fragment.fileStream = null;
return;
}
// 转为blob
const blob = ArrayBufferToBlob(buffer, { type: fragment.contentType });
// 发送到ffmpeg
if (_ffmpeg) {
sendFile(_ffmpeg, blob, fragment);
$dom.progressText.html(i18n.sendFfmpeg);
return;
}
$dom.progressText.html(i18n.saving);
// 直接下载
chrome.downloads.download({
url: URL.createObjectURL(blob),
filename: fragment.downFileName,
saveAs: G.saveAs
}, function (downloadId) {
fragment.downId = downloadId;
});
});
// 全部下载完成事件
down.on('allCompleted', function (buffer) {
$("#stopDownload").hide();
// 检查 down.fragments 是否都为边下边存 检查自动关闭
if (down.fragments.every(item => item.fileStream) && $("#autoClose").prop("checked")) {
setTimeout(() => {
closeTab();
}, Math.ceil(Math.random() * 999));
}
});
// 错误处理
down.on('downloadError', function (fragment, error) {
// 添加range请求头 重新尝试下载
if (!fragment.retry?.Range && error?.cause == "HTTPError") {
fragment.retry = { "Range": "bytes=0-" };
down.stop(fragment.index);
down.downloader(fragment);
return;
}
// 添加sec-fetch 再次尝试下载
if (!fragment.retry?.sec && error?.cause == "HTTPError") {
fragment.retry.sec = true;
if (!fragment.requestHeaders) { fragment.requestHeaders = {}; }
fragment.requestHeaders = { ...fragment.requestHeaders, "sec-fetch-mode": "no-cors", "sec-fetch-site": "same-site" };
setHeaders(fragment, () => { down.stop(fragment.index); down.downloader(fragment); }, _tabId);
return;
}
itemDOM.get(fragment.index).progressText.html(error);
chrome.tabs.highlight({ tabs: _index });
});
// 开始下载事件 如果存在range重下标记 则添加 range 请求头
down.on('start', function (fragment, options) {
if (fragment.retry) {
options.headers = fragment.retry;
options.cache = "no-cache";
}
});
// 全部停止下载按钮
$("#stopDownload").click(function () {
down.stop();
// 更新对应的按钮状态
itemDOM.forEach((item, index) => {
if (item.button.data("action") == "stop") {
item.button.html(i18n.retryDownload).data("action", "start");
if (down.fragments[index].fileStream) {
down.fragments[index].fileStream.close();
down.fragments[index].fileStream = null;
}
}
});
});
// 打开下载目录
$(".openDir").click(function () {
if (down.fragments[0].downId) {
chrome.downloads.show(down.fragments[0].downId);
return;
}
chrome.downloads.showDefaultFolder();
});
// 监听事件
chrome.runtime.onMessage.addListener(function (Message, sender, sendResponse) {
if (!Message.Message) { return; }
// 外部添加下载任务
if (Message.Message == "catDownload" && Message.data && Array.isArray(Message.data)) {
// ffmpeg任务的下载器 不允许添加新任务
if (_ffmpeg) {
sendResponse({ message: "FFmpeg", tabId: _tabId });
return;
}
setHeaders(Message.data, () => {
for (let fragment of Message.data) {
// 检查fragment是否已经存在
if (down.fragments.find(item => item.requestId == fragment.requestId)) {
continue;
}
_data.push(fragment);
down.push(fragment);
addHtml(fragment);
// 修改url requestId 参数
const url = new URL(location.href);
url.searchParams.set("requestId", down.fragments.map(item => item.requestId).join(","));
history.replaceState(null, null, url);
// 数据储存到localStorage
downloadData.push(fragment);
localStorage.setItem('downloadData', JSON.stringify(downloadData));
// 正在运行的下载任务小于线程数 则开始下载
if (down.running < down.thread) {
// down.downloader(fragment.index);
down.downloader();
}
};
}, _tabId);
sendResponse({ message: "OK", tabId: _tabId });
return;
}
// 以下为在线ffmpeg返回结果
if (Message.Message != "catCatchFFmpegResult" || Message.state != "ok" || _tabId == 0 || Message.tabId != _tabId) { return; }
// 发送状态提示
const $dom = itemDOM.get(Message.index);
$dom && $dom.progressText.html(i18n.hasSent);
down.buffer[Message.index] = null; //清空buffer
// 全部发送完成 检查自动关闭
if (down.success == down.total) {
if ($("#autoClose").prop("checked")) {
setTimeout(() => {
closeTab();
}, Math.ceil(Math.random() * 999));
}
}
});
// 监听下载事件 下载完成 关闭窗口
chrome.downloads.onChanged.addListener(function (downloadDelta) {
if (!downloadDelta.state || downloadDelta.state.current != "complete") { return; }
// 检查id是否本页面提交的下载
const fragment = down.fragments.find(item => item.downId == downloadDelta.id);
if (!fragment) { return; }
down.buffer[fragment.index] = null; //清空buffer
// 更新下载状态
itemDOM.get(fragment.index).progressText.html(i18n.downloadComplete);
// 完成下载 检查自动关闭
if (down.success == down.total) {
document.title = i18n.downloadComplete;
if ($("#autoClose").prop("checked")) {
setTimeout(() => {
closeTab();
}, Math.ceil(Math.random() * 999));
}
}
});
// 关闭页面 检查关闭所有未完成的下载流
window.addEventListener('beforeunload', function (e) {
const fileStream = down.fragments.filter(item => item.fileStream);
if (fileStream.length) {
e.preventDefault();
fileStream.forEach((fragment) => {
fragment.fileStream.close();
});
}
});
document.title = `${down.success}/${down.total}`;
down.start();
}
/**
* 发送数据到在线FFmpeg
* @param {String} action 发送类型
* @param {ArrayBuffer|Blob} data 数据内容
* @param {Object} fragment 数据对象
*/
let isCreatingTab = false;
function sendFile(action, data, fragment) {
// 转 blob
if (data instanceof ArrayBuffer) {
data = ArrayBufferToBlob(data, { type: fragment.contentType });
}
chrome.tabs.query({ url: G.ffmpegConfig.url + "*" }, function (tabs) {
// 等待ffmpeg 打开并且可用
if (tabs.length === 0) {
if (!isCreatingTab) {
isCreatingTab = true; // 设置创建标志位
chrome.tabs.create({ url: G.ffmpegConfig.url });
}
setTimeout(sendFile, 500, action, data, fragment);
return;
} else if (tabs[0].status !== "complete") {
setTimeout(sendFile, 233, action, data, fragment);
return;
}
isCreatingTab = false; // 重置创建标志位
/**
* https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Chrome_incompatibilities#data_cloning_algorithm
* chrome.runtime.sendMessage API
* chrome 的对象参数需要序列化 无法传递Blob
* firefox 可以直接传递Blob
*/
const baseData = {
Message: "catCatchFFmpeg",
action: action,
files: [{ data: G.isFirefox ? data : URL.createObjectURL(data), name: getUrlFileName(fragment.url), index: fragment.index }],
title: stringModify(fragment.title),
tabId: _tabId
};
if (action === "merge") {
baseData.taskId = _taskId;
baseData.quantity = _data.length;
}
chrome.runtime.sendMessage(baseData);
});
}

22
js/firefox.js Normal file
View File

@@ -0,0 +1,22 @@
// 兼容Firefox
if (typeof (browser) == "object") {
function importScripts() {
for (let script of arguments) {
const js = document.createElement('script');
js.src = script;
document.head.appendChild(js);
}
}
// browser.windows.onFocusChanged.addListener 少一个参数
const _onFocusChanged = chrome.windows.onFocusChanged.addListener;
chrome.windows.onFocusChanged.addListener = function (listener, option) {
_onFocusChanged(listener);
};
browser.runtime.onInstalled.addListener(({ reason }) => {
if (reason == "install") {
browser.tabs.create({ url: "install.html" });
}
});
}

736
js/function.js Normal file
View File

@@ -0,0 +1,736 @@
/**
* 小于10的数字前面加0
* @param {Number} date
* @returns {String|Number}
*/
function appendZero(date) {
return parseInt(date) < 10 ? `0${date}` : date;
}
/**
* 秒转格式化成时间
* @param {Number} sec
* @returns {String}
*/
function secToTime(sec) {
let hour = (sec / 3600) | 0;
let min = ((sec % 3600) / 60) | 0;
sec = (sec % 60) | 0;
let time = hour > 0 ? hour + ":" : "";
time += min.toString().padStart(2, '0') + ":";
time += sec.toString().padStart(2, '0');
return time;
}
/**
* 字节转换成大小
* @param {Number} byte 大小
* @returns {String} 格式化后的文件大小
*/
function byteToSize(byte) {
if (!byte || byte < 1024) { return 0; }
if (byte < 1024 * 1024) {
return (byte / 1024).toFixed(1) + "KB";
} else if (byte < 1024 * 1024 * 1024) {
return (byte / 1024 / 1024).toFixed(1) + "MB";
} else {
return (byte / 1024 / 1024 / 1024).toFixed(1) + "GB";
}
}
/**
* Firefox download API 无法下载 data URL
* @param {String} url
* @param {String} fileName 文件名
*/
function downloadDataURL(url, fileName) {
let link = document.createElement("a");
link.href = url;
link.download = fileName;
link.click();
link = null;
}
/**
* 判断变量是否为空
* @param {Object|String} obj 判断的变量
* @returns {Boolean}
*/
function isEmpty(obj) {
return (typeof obj == "undefined" ||
obj == null ||
obj == "" ||
obj == " ")
}
/**
* 修改请求头
* @param {Object} data 请求头数据
* @param {Function} callback
*/
function setRequestHeaders(data = {}, callback = undefined) {
chrome.declarativeNetRequest.updateSessionRules({ removeRuleIds: [1] });
chrome.tabs.getCurrent(function (tabs) {
const rules = { removeRuleIds: [tabs ? tabs.id : 1] };
if (Object.keys(data).length) {
rules.addRules = [{
"id": tabs ? tabs.id : 1,
"priority": tabs ? tabs.id : 1,
"action": {
"type": "modifyHeaders",
"requestHeaders": Object.keys(data).map(key => ({ header: key, operation: "set", value: data[key] }))
},
"condition": {
"resourceTypes": ["xmlhttprequest", "media", "image"],
}
}];
if (tabs) {
rules.addRules[0].condition.tabIds = [tabs.id];
} else {
// initiatorDomains 只支持 chrome 101+ firefox 113+
if (G.version < 101 || (G.isFirefox && G.version < 113)) {
callback && callback();
return;
}
const domain = G.isFirefox
? new URL(chrome.runtime.getURL("")).hostname
: chrome.runtime.id;
rules.addRules[0].condition.initiatorDomains = [domain];
}
}
chrome.declarativeNetRequest.updateSessionRules(rules, function () {
callback && callback();
});
});
}
/**
* 指定标签页修改 urlFilter请求头
* @param {Object} data 需要修改请求头的对象数组
* @param {*} callBack 回调函数
* @param {*} tabId 需要修改的tabId
*/
function setHeaders(data, callBack, tabId = -1) {
if (!tabId == -1) {
tabId = G.tabId;
}
const rules = { removeRuleIds: [], addRules: [] };
if (!Array.isArray(data)) {
data = [data];
}
for (let item of data) {
if (!item.requestHeaders) { continue; }
const rule = {
"id": parseInt(item.requestId),
"action": {
"type": "modifyHeaders",
"requestHeaders": Object.keys(item.requestHeaders).map(key => ({ header: key, operation: "set", value: item.requestHeaders[key] }))
},
"condition": {
"resourceTypes": ["xmlhttprequest", "media", "image"],
"tabIds": [tabId],
"urlFilter": item.url
}
}
if (item.cookie) {
rule.action.requestHeaders.push({ header: "Cookie", operation: "set", value: item.cookie });
}
rules.removeRuleIds.push(parseInt(item.requestId));
rules.addRules.push(rule);
}
chrome.declarativeNetRequest.updateSessionRules(rules, () => {
callBack && callBack();
});
}
/**
* 等待全局变量G初始化完成
* @param {Function} callback
* @param {Number} sec
*/
function awaitG(callback, sec = 0) {
const timer = setInterval(() => {
if (G.initSyncComplete && G.initLocalComplete) {
clearInterval(timer);
callback();
}
}, sec);
}
/**
* 分割字符串 不分割引号内的内容
* @param {String} text 需要处理的文本
* @param {String} separator 分隔符
* @returns {String} 返回分割后的字符串
*/
function splitString(text, separator) {
text = text.trim();
if (text.length == 0) { return []; }
const parts = [];
let inQuotes = false;
let inSingleQuotes = false;
let start = 0;
for (let i = 0; i < text.length; i++) {
if (text[i] === separator && !inQuotes && !inSingleQuotes) {
parts.push(text.slice(start, i));
start = i + 1;
} else if (text[i] === '"' && !inSingleQuotes) {
inQuotes = !inQuotes;
} else if (text[i] === "'" && !inQuotes) {
inSingleQuotes = !inSingleQuotes;
}
}
parts.push(text.slice(start));
return parts;
}
/**
* 模板的函数处理
* @param {String} text 文本
* @param {String} action 函数名
* @param {Object} data 填充的数据
* @returns {String} 返回处理后的字符串
*/
function templatesFunction(text, action, data) {
text = isEmpty(text) ? "" : text.toString();
action = splitString(action, "|");
for (let item of action) {
let action = item.trim(); // 函数
let arg = []; //参数
// 查找 ":" 区分函数与参数
const colon = item.indexOf(":");
if (colon != -1) {
action = item.slice(0, colon).trim();
arg = splitString(item.slice(colon + 1).trim(), ",").map(item => {
return item.trim().replace(/^['"]|['"]$/g, "");
});
}
// 字符串不允许为空 除非 exists find prompt函数
if (isEmpty(text) && !["exists", "find", "prompt"].includes(action)) { return "" };
// 参数不能为空 除非 filter prompt函数
if (arg.length == 0 && !["filter", "prompt"].includes(action)) { return text }
if (action == "slice") {
text = text.slice(...arg);
} else if (action == "replace") {
text = text.replace(...arg);
} else if (action == "replaceAll") {
text = text.replaceAll(...arg);
} else if (action == "regexp") {
const result = text.match(new RegExp(...arg));
text = "";
if (result && result.length >= 2) {
for (let i = 1; i < result.length; i++) {
if (result[i]) {
text += result[i].trim();
}
}
}
} else if (action == "exists") {
if (text) {
text = arg[0].replaceAll("*", text);
continue;
}
if (arg[1]) {
text = arg[1].replaceAll("*", text);
continue;
}
text = "";
} else if (action == "prepend") {
text = arg[0] + text;
} else if (action == "concat") {
text = text + arg[0];
} else if (action == "to") {
if (arg[0] == "base64") {
text = window.Base64 ? Base64.encode(text) : btoa(unescape(encodeURIComponent(text)));
} else if (arg[0] == "urlEncode") {
text = encodeURIComponent(text);
} else if (arg[0] == "urlDecode") {
text = decodeURIComponent(text);
} else if (arg[0] == "lowerCase") {
text = text.toLowerCase();
} else if (arg[0] == "upperCase") {
text = text.toUpperCase();
} else if (arg[0] == "trim") {
if (text) { text = text.trim(); }
} else if (arg[0] == "filter") {
if (text) { text = text.trim(); }
text = stringModify(text);
}
} else if (action == "find") {
text = "";
if (data.pageDOM) {
try {
text = data.pageDOM.querySelector(arg[0]).innerText?.trim();
} catch (e) { text = ""; }
}
} else if (action == "filter") {
text = stringModify(text, arg[0]);
} else if (action == "prompt") {
text = window.prompt("", text);
}
}
return text;
}
/**
* 模板替换
* @param {String} text 标签模板
* @param {Object} data 填充的数据
* @returns {String} 返回填充后的字符串
*/
function templates(text, data) {
if (isEmpty(text)) { return ""; }
// fullFileName
try {
data.fullFileName = new URL(data.url).pathname.split("/").pop();
} catch (e) {
data.fullFileName = 'NULL';
}
// fileName
data.fileName = data.fullFileName.split(".");
data.fileName.length > 1 && data.fileName.pop();
data.fileName = data.fileName.join(".");
// ext
if (isEmpty(data.ext)) {
data.ext = data.fullFileName.split(".");
data.ext = data.ext.length == 1 ? "" : data.ext[data.ext.length - 1];
}
const date = new Date();
const trimData = {
// 资源信息
url: data.url ?? "",
referer: data.requestHeaders?.referer ?? "",
origin: data.requestHeaders?.origin ?? "",
initiator: data.requestHeaders?.referer ? data.requestHeaders.referer : data.initiator,
webUrl: data.webUrl ?? "",
title: data._title ?? data.title,
pageDOM: data.pageDOM,
cookie: data.cookie ?? "",
tabId: data.tabId ?? 0,
// 时间相关
year: date.getFullYear(),
month: appendZero(date.getMonth() + 1),
date: appendZero(date.getDate()),
day: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][date.getDay()],
fullDate: `${date.getFullYear()}-${appendZero(date.getMonth() + 1)}-${appendZero(date.getDate())}`,
time: `${appendZero(date.getHours())}'${appendZero(date.getMinutes())}'${appendZero(date.getSeconds())}`,
hours: appendZero(date.getHours()),
minutes: appendZero(date.getMinutes()),
seconds: appendZero(date.getSeconds()),
now: Date.now(),
timestamp: new Date().toISOString(),
// 文件名
fullFileName: data.fullFileName ? data.fullFileName : "",
fileName: data.fileName ? data.fileName : "",
ext: data.ext ?? "",
// 全局变量
mobileUserAgent: G.MobileUserAgent,
userAgent: G.userAgent ? G.userAgent : navigator.userAgent,
}
const _data = { ...data, ...trimData };
text = text.replace(reTemplates, function (original, tag, action) {
tag = tag.trim();
// 特殊标签 data 返回所有数据
if (tag == 'data') { return JSON.stringify(trimData); }
if (action) {
return templatesFunction(_data[tag], action.trim(), _data);
}
return _data[tag] ?? original;
});
return text;
}
/**
* 从url中获取文件名
* @param {String} url
* @returns {String} 文件名
*/
function getUrlFileName(url) {
let pathname = new URL(url).pathname;
let filename = pathname.split("/").pop();
return filename ? filename : "NULL";
}
/**
* 解析json字符串 尝试修复键名没有双引号 解析错误返回默认值
* @param {string} str json字符串
* @param {object} error 解析错误返回的默认值
* @param {number} attempt 尝试修复次数
* @returns {object} 返回解析后的对象
*/
function JSONparse(str, error = {}, attempt = 0) {
if (!str) { return error; }
try {
return JSON.parse(str);
} catch (e) {
if (attempt === 0) {
// 第一次解析失败,修正字符串后递归调用
reJSONparse.lastIndex = 0;
const fixedStr = str.replace(reJSONparse, '$1"$2"$3');
return JSONparse(fixedStr, error, ++attempt);
} else {
// 第二次解析仍然失败,返回 error 对象
return error;
}
}
}
/**
* ArrayBuffer转Blob 大于2G的做切割
* @param {ArrayBuffer|Uint8Array} buffer 原始数据
* @param {Object} options Blob配置
* @returns {Blob} 返回Blob对象
*/
function ArrayBufferToBlob(buffer, options = {}) {
if (buffer instanceof Blob) {
return buffer;
}
if (buffer instanceof Uint8Array) {
buffer = buffer.buffer;
}
if (!buffer.byteLength) {
return new Blob();
}
if (!buffer instanceof ArrayBuffer) {
return new Blob();
}
if (buffer.byteLength >= 2 * 1024 * 1024 * 1024) {
const MAX_CHUNK_SIZE = 1024 * 1024 * 1024;
let offset = 0;
const blobs = [];
while (offset < buffer.byteLength) {
const chunkSize = Math.min(MAX_CHUNK_SIZE, buffer.byteLength - offset);
const chunk = buffer.slice(offset, offset + chunkSize);
blobs.push(new Blob([chunk]));
offset += chunkSize;
}
return new Blob(blobs, options);
}
return new Blob([buffer], options);
}
/**
* 清理冗余数据
*/
function clearRedundant() {
chrome.tabs.query({}, function (tabs) {
const allTabId = new Set(tabs.map(tab => tab.id));
if (!cacheData.init) {
// 清理 缓存数据
let cacheDataFlag = false;
for (let key in cacheData) {
if (!allTabId.has(Number(key))) {
cacheDataFlag = true;
delete cacheData[key];
}
}
cacheDataFlag && (chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });
}
// 清理
G.urlMap.forEach((_, key) => {
!allTabId.has(key) && G.urlMap.delete(key);
});
// 清理脚本
G.scriptList.forEach(function (scriptList) {
scriptList.tabId.forEach(function (tabId) {
if (!allTabId.has(tabId)) {
scriptList.tabId.delete(tabId);
}
});
});
if (!G.initLocalComplete) { return; }
// 清理 declarativeNetRequest 模拟手机
chrome.declarativeNetRequest.getSessionRules(function (rules) {
let mobileFlag = false;
for (let item of rules) {
if (item.condition.tabIds) {
// 如果tabIds列表都不存在 则删除该条规则
if (!item.condition.tabIds.some(id => allTabId.has(id))) {
mobileFlag = true;
item.condition.tabIds.forEach(id => G.featMobileTabId.delete(id));
chrome.declarativeNetRequest.updateSessionRules({
removeRuleIds: [item.id]
});
}
} else if (item.id == 1) {
// 清理预览视频增加的请求头
chrome.declarativeNetRequest.updateSessionRules({ removeRuleIds: [1] });
}
}
mobileFlag && (chrome.storage.session ?? chrome.storage.local).set({ featMobileTabId: Array.from(G.featMobileTabId) });
});
// 清理自动下载
let autoDownFlag = false;
G.featAutoDownTabId.forEach(function (tabId) {
if (!allTabId.has(tabId)) {
autoDownFlag = true;
G.featAutoDownTabId.delete(tabId);
}
});
autoDownFlag && (chrome.storage.session ?? chrome.storage.local).set({ featAutoDownTabId: Array.from(G.featAutoDownTabId) });
G.blockUrlSet = new Set([...G.blockUrlSet].filter(x => allTabId.has(x)));
if (G.requestHeaders.size >= 10240) {
G.requestHeaders.clear();
}
});
// G.referer.clear();
// G.blackList.clear();
// G.temp.clear();
}
/**
* 替换掉文件名中的特殊字符 包含路径
* @param {String} str 需要处理的文本
* @param {String} text 需要替换的文本
* @returns {String} 返回替换后的字符串
*/
function stringModify(str, text) {
if (!str) { return str; }
str = filterFileName(str, text);
return str.replaceAll("\\", "&bsol;").replaceAll("/", "&sol;");
}
/**
* 替换掉文件名中的特殊字符 不包含路径
* @param {String} str 需要处理的文本
* @param {String} text 需要替换的文本
* @returns {String} 返回替换后的字符串
*/
function filterFileName(str, text) {
if (!str) { return str; }
reFilterFileName.lastIndex = 0;
str = str.replaceAll(/\u200B/g, "").replaceAll(/\u200C/g, "").replaceAll(/\u200D/g, "");
str = str.replace(reFilterFileName, function (match) {
return text || {
'<': '&lt;',
'>': '&gt;',
':': '&colon;',
'"': '&quot;',
'|': '&vert;',
'?': '&quest;',
'*': '&ast;',
'~': '_'
}[match];
});
// 前后不能是 “.”
if (str.endsWith(".")) {
str = str + "catCatch";
}
if (str.startsWith(".")) {
str = "catCatch" + str;
}
return str;
}
/**
* 展平嵌套对象的函数
* @param {Object} obj 参数对象
* @param {String} prefix 前缀
* @returns 嵌套对象扁平化
*/
function flattenObject(obj, prefix = '') {
let result = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const value = obj[key];
const newKey = prefix ? `${prefix}[${key}]` : key;
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
// 递归处理嵌套对象
Object.assign(result, flattenObject(value, newKey));
} else {
// 处理基本类型和数组
result[newKey] = value;
}
}
}
return result;
}
/**
* 发送数据到本地
* @param {String} action 发送类型
* @param {Object|Srting} data 发送的数据
* @param {Number} tabId 发送数据的标签页ID
*/
function send2local(action, data, tabId = 0) {
return new Promise((resolve, reject) => {
// 请求方式
const option = { method: G.send2localMethod };
// 处理替换模板
let body = G.send2localBody;
// 处理 addKey 请求
if (action == 'addKey' || typeof data === 'string') {
body = G.send2localBody.replaceAll('${data}', `"${data}"`);
data = { tabId: tabId };
}
data.action = action;
let postData = templates(body, data);
// 转为对象
postData = JSONparse(postData, { action, data, tabId });
try {
// 处理URL中的模板字符串并检查合法性
let send2localURL = templates(G.send2localURL, data);
send2localURL = new URL(send2localURL);
// GET请求拼接参数
if (option.method === 'GET') {
const flattenedObj = flattenObject(postData);
const urlParams = new URLSearchParams(flattenedObj);
send2localURL.search = send2localURL.search
? `${send2localURL.search}&${urlParams}`
: `?${urlParams}`;
}
// 非GET请求处理不同Content-Type
else {
const contentType = {
0: 'application/json;charset=utf-8',
1: 'multipart/form-data',
2: 'application/x-www-form-urlencoded',
3: 'text/plain'
}[G.send2localType];
option.headers = { 'Content-Type': contentType };
switch (contentType) {
case 'application/json;charset=utf-8':
option.body = JSON.stringify(postData);
break;
case 'multipart/form-data':
const formData = new FormData();
const flattened = flattenObject(postData);
Object.entries(flattened).forEach(([key, value]) => {
formData.append(key, value);
});
option.body = formData;
delete option.headers['Content-Type']; // 浏览器自动生成boundary
break;
case 'application/x-www-form-urlencoded':
const flattenedObj = flattenObject(postData);
const urlParams = new URLSearchParams(flattenedObj);
option.body = urlParams.toString();
break;
case 'text/plain':
option.body = typeof postData === 'object'
? JSON.stringify(postData)
: String(postData);
break;
default:
option.body = JSON.stringify(postData);
break;
}
}
send2localURL = send2localURL.toString();
fetch(send2localURL, option)
.then(response => resolve(response))
.catch(error => reject(error));
} catch (e) {
reject(e);
}
});
}
/**
* 判断url是否在屏蔽网址中
* @param {String} url
* @returns {Boolean}
*/
function isLockUrl(url) {
for (let key in G.blockUrl) {
if (!G.blockUrl[key].state) { continue; }
G.blockUrl[key].url.lastIndex = 0;
if (G.blockUrl[key].url.test(url)) {
return true;
}
}
return false;
}
/**
* 关闭标签页 如果tabId为0 则关闭当前标签
* 当前只有一个标签页面 创建新标签页 再关闭
* @param {Number|Array} tabId
*/
function closeTab(tabId = 0) {
chrome.tabs.query({}, async function (tabs) {
if (tabs.length === 1) {
await chrome.tabs.create({ url: 'chrome://newtab' });
tabId ? chrome.tabs.remove(tabId) : window.close();
} else {
tabId ? chrome.tabs.remove(tabId) : window.close();
}
});
}
/**
* 打开解析器
* @param {Object} data 资源对象
* @param {Object} options 选项
*/
function openParser(data, options = {}) {
chrome.tabs.get(G.tabId, function (tab) {
const url = `/${data.parsing ? data.parsing : "m3u8"}.html?${new URLSearchParams({
url: data.url,
title: data.title,
filename: data.downFileName,
tabid: data.tabId == -1 ? G.tabId : data.tabId,
initiator: data.initiator,
requestHeaders: data.requestHeaders ? JSON.stringify(data.requestHeaders) : undefined,
...Object.fromEntries(Object.entries(options).map(([key, value]) => [key, typeof value === 'boolean' ? 1 : value])),
})}`
chrome.tabs.create({
url: url,
index: tab.index + 1,
active: G.isMobile || !options.autoDown
});
});
}
/**
* 加载CSS样式
*/
function loadCSS() {
if (G.isMobile) {
const mobileCssLink = document.createElement('link');
mobileCssLink.rel = 'stylesheet';
mobileCssLink.type = 'text/css';
mobileCssLink.href = 'css/mobile.css';
document.head.appendChild(mobileCssLink);
}
const styleElement = document.createElement('style');
styleElement.textContent = G.css;
document.head.appendChild(styleElement);
}
/**
* 修建数据 不发送不必要的数据
* @param {Object} originalData 原始数据
* @returns {Object} 返回处理后的数据
*/
function trimData(originalData) {
const data = { ...originalData };
// 不发送HTML内容
data.html = undefined;
data.panelHeading = undefined;
data.urlPanel = undefined;
data.urlPanelShow = undefined;
return data;
}

15
js/i18n.js Normal file
View File

@@ -0,0 +1,15 @@
(function () {
document.querySelectorAll('[data-i18n]').forEach(function (element) {
element.innerHTML = i18n(element.dataset.i18n) ?? element.dataset.i18n;
});
document.querySelectorAll('[data-i18n-outer]').forEach(function (element) {
element.outerHTML = i18n(element.dataset.i18nOuter) ?? element.dataset.i18nOuter;
});
document.querySelectorAll('i18n').forEach(function (element) {
element.outerHTML = i18n(element.innerHTML) ?? element.innerHTML;
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(function (element) {
element.setAttribute('placeholder', i18n(element.dataset.i18nPlaceholder) ?? element.dataset.i18nPlaceholder);
});
document.title = i18n(document.title) ?? document.title;
})();

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

64
js/install.js Normal file
View File

@@ -0,0 +1,64 @@
window.addEventListener('DOMContentLoaded', function () {
// 多语言支持
let currentLang = 'zh';
function setLanguage(lang) {
currentLang = lang;
// 设置活动语言
document.querySelectorAll('.lang-zh').forEach(el => {
el.classList.toggle('active', lang === 'zh');
});
document.querySelectorAll('.lang-en').forEach(el => {
el.classList.toggle('active', lang === 'en');
});
// 更新语言切换按钮
document.getElementById('langText').textContent = lang === 'zh' ? 'English' : '中文';
document.getElementById('langEmoji').textContent = lang === 'zh' ? '🌐' : '🇨🇳';
// 更新动态文本
if (lang === 'en') {
document.getElementById('main-title').textContent = 'Installation Successful!';
document.getElementById('subtitle').textContent = 'Cat Catch Extension is now installed';
document.getElementById('welcome-title').textContent = 'Welcome to Cat Catch';
document.getElementById('privacy-title').textContent = 'Privacy Policy';
document.getElementById('disclaimer-title').textContent = 'Disclaimer';
document.getElementById('issue-title').textContent = 'Issue Submission';
document.getElementById('agreeText').textContent = 'Agree';
document.getElementById('uninstallText').textContent = 'Uninstall Extension';
} else {
document.getElementById('main-title').textContent = '恭喜 猫抓 扩展已成功安装 !';
document.getElementById('subtitle').textContent = 'Installation successful !';
document.getElementById('welcome-title').textContent = '希望本扩展能帮助到你';
document.getElementById('privacy-title').textContent = '隐私政策 / Privacy Policy';
document.getElementById('disclaimer-title').textContent = '免责声明 / Disclaimer';
document.getElementById('issue-title').textContent = '问题提交 / Issue Submission';
document.getElementById('agreeText').textContent = '同意';
document.getElementById('uninstallText').textContent = '卸载扩展';
}
}
// 语言切换功能
document.getElementById('langToggle').addEventListener('click', function () {
const newLang = currentLang === 'zh' ? 'en' : 'zh';
setLanguage(newLang);
});
// 按钮事件处理
document.getElementById('agreeBtn').addEventListener('click', function () {
closeTab();
});
document.getElementById('uninstallBtn').addEventListener('click', function () {
chrome.management.uninstallSelf({ showConfirmDialog: true });
});
const lang = navigator.language || navigator.userLanguage;
const isChinese = lang.startsWith('zh');
setLanguage(isChinese ? 'zh' : 'en');
// 添加动画效果
document.querySelector('.card').classList.add('fade-in');
document.getElementById('agreeBtn').focus();
});

85
js/json.js Normal file
View File

@@ -0,0 +1,85 @@
// url 参数解析
const params = new URL(location.href).searchParams;
var _url = params.get("url");
// const _referer = params.get("referer");
const _requestHeaders = params.get("requestHeaders");
// 修改当前标签下的所有xhr的requestHeaders
let requestHeaders = JSONparse(_requestHeaders);
setRequestHeaders(requestHeaders, () => { awaitG(init); });
function init() {
loadCSS();
var jsonContent = "";
var options = {
collapsed: true,
rootCollapsable: false,
withQuotes: false,
withLinks: true
};
if (isEmpty(_url)) {
$("#jsonCustom").show(); $("#main").hide();
$("#format").click(function () {
_url = $("#jsonUrl").val().trim();
if (isEmpty(_url)) {
let jsonText = $("#jsonText").val();
jsonContent = JSON.parse(jsonText);
renderJson();
$("#jsonCustom").hide(); $("#main").show();
return;
}
getJson(_url);
});
} else {
getJson(_url);
}
function getJson(url) {
$("#jsonCustom").hide(); $("#main").show();
$.ajax({
url: url,
dataType: "text",
}).fail(function (result) {
console.log(result);
$('#json-renderer').html(i18n.fileRetrievalFailed);
$("#collapsed").hide();
}).done(function (result) {
// console.log(result);
result = result.replace(/^try{/, "").replace(/}catch\(e\){.*}$/ig, ""); //去除try{}catch(e){}
try {
jsonContent = JSON.parse(result);
} catch (e) {
console.log(e);
let regexp = [
/^.*=({.*}).*$/,
/^.*\(({.*})\).*$/
]
for (let regex of regexp) {
let res = new RegExp(regex, "ig").exec(result);
if (res) {
// console.log(res);
result = res[1];
break;
}
}
// console.log(result);
jsonContent = JSON.parse(result);
}
renderJson();
});
}
function renderJson() {
$('#json-renderer').jsonViewer(jsonContent, options);
}
$("#collapsed").click(function () {
options.collapsed = !options.collapsed;
if (options.collapsed) {
collapsed.innerHTML = i18n.expandAllNodes;
} else {
collapsed.innerHTML = i18n.collapseAllNodes;
}
renderJson();
});
}

333
js/m3u8.downloader.js Normal file
View File

@@ -0,0 +1,333 @@
class Downloader {
constructor(fragments = [], thread = 6) {
this.fragments = fragments; // 切片列表
this.allFragments = fragments; // 储存所有原始切片列表
this.thread = thread; // 线程数
this.events = {}; // events
this.decrypt = null; // 解密函数
this.transcode = null; // 转码函数
this.init();
}
/**
* 初始化所有变量
*/
init() {
this.index = 0; // 当前任务索引
this.buffer = []; // 储存的buffer
this.state = 'waiting'; // 下载器状态 waiting running done abort
this.success = 0; // 成功下载数量
this.errorList = new Set(); // 下载错误的列表
this.buffersize = 0; // 已下载buffer大小
this.duration = 0; // 已下载时长
this.pushIndex = 0; // 推送顺序下载索引
this.controller = []; // 储存中断控制器
this.running = 0; // 正在下载数量
}
/**
* 设置监听
* @param {string} eventName 监听名
* @param {Function} callBack
*/
on(eventName, callBack) {
if (this.events[eventName]) {
this.events[eventName].push(callBack);
} else {
this.events[eventName] = [callBack];
}
}
/**
* 触发监听器
* @param {string} eventName 监听名
* @param {...any} args
*/
emit(eventName, ...args) {
if (this.events[eventName]) {
this.events[eventName].forEach(callBack => {
callBack(...args);
});
}
}
/**
* 设定解密函数
* @param {Function} callback
*/
setDecrypt(callback) {
this.decrypt = callback;
}
/**
* 设定转码函数
* @param {Function} callback
*/
setTranscode(callback) {
this.transcode = callback;
}
/**
* 停止下载 没有目标 停止所有线程
* @param {number} index 停止下载目标
*/
stop(index = undefined) {
if (index !== undefined) {
this.controller[index] && this.controller[index].abort();
return;
}
this.controller.forEach(controller => { controller.abort() });
this.state = 'abort';
}
/**
* 检查对象是否错误列表内
* @param {object} fragment 切片对象
* @returns {boolean}
*/
isErrorItem(fragment) {
return this.errorList.has(fragment);
}
/**
* 返回所有错误列表
*/
get errorItem() {
return this.errorList;
}
/**
* 按照顺序推送buffer数据
*/
sequentialPush() {
if (!this.events["sequentialPush"]) { return; }
for (; this.pushIndex < this.fragments.length; this.pushIndex++) {
if (this.buffer[this.pushIndex]) {
this.emit('sequentialPush', this.buffer[this.pushIndex]);
delete this.buffer[this.pushIndex];
continue;
}
break;
}
}
/**
* 限定下载范围
* @param {number} start 下载范围 开始索引
* @param {number} end 下载范围 结束索引
* @returns {boolean}
*/
range(start = 0, end = this.fragments.length) {
if (start > end) {
this.emit('error', 'start > end');
return false;
}
if (end > this.fragments.length) {
this.emit('error', 'end > total');
return false;
}
if (start >= this.fragments.length) {
this.emit('error', 'start >= total');
return false;
}
if (start != 0 || end != this.fragments.length) {
this.fragments = this.fragments.slice(start, end);
// 更改过下载范围 重新设定index
this.fragments.forEach((fragment, index) => {
fragment.index = index;
});
}
// 总数为空 抛出错误
if (this.fragments.length == 0) {
this.emit('error', 'List is empty');
return false;
}
return true;
}
/**
* 获取切片总数量
* @returns {number}
*/
get total() {
return this.fragments.length;
}
/**
* 获取切片总时间
* @returns {number}
*/
get totalDuration() {
return this.fragments.reduce((total, fragment) => total + fragment.duration, 0);
}
/**
* 切片对象数组的 setter getter
*/
set fragments(fragments) {
// 增加index参数 为多线程异步下载 根据index属性顺序保存
this._fragments = fragments.map((fragment, index) => ({ ...fragment, index }));
}
get fragments() {
return this._fragments;
}
/**
* 获取 #EXT-X-MAP 标签的文件url
* @returns {string}
*/
get mapTag() {
if (this.fragments[0].initSegment && this.fragments[0].initSegment.url) {
return this.fragments[0].initSegment.url;
}
return "";
}
/**
* 添加一条新资源
* @param {Object} fragment
*/
push(fragment) {
fragment.index = this.fragments.length;
this.fragments.push(fragment);
}
/**
* 下载器 使用fetch下载文件
* @param {object} fragment 重新下载的对象
*/
downloader(fragment = null) {
if (this.state === 'abort') { return; }
// 是否直接下载对象
const directDownload = !!fragment;
// 非直接下载对象 从this.fragments获取下一条资源 若不存在跳出
if (!directDownload && !this.fragments[this.index]) { return; }
// fragment是数字 直接从this.fragments获取
if (typeof fragment === 'number') {
fragment = this.fragments[fragment];
}
// 不存在下载对象 从提取fragments
fragment ??= this.fragments[this.index++];
this.state = 'running';
this.running++;
// 资源已下载 跳过
// if (this.buffer[fragment.index]) { return; }
// 停止下载控制器
const controller = new AbortController();
this.controller[fragment.index] = controller;
const options = { signal: controller.signal };
// 下载前触发事件
this.emit('start', fragment, options);
// 开始下载
fetch(fragment.url, options)
.then(response => {
if (!response.ok) {
throw new Error(response.status, { cause: 'HTTPError' });
}
const reader = response.body.getReader();
const contentLength = parseInt(response.headers.get('content-length')) || 0;
fragment.contentType = response.headers.get('content-type') ?? 'null';
let receivedLength = 0;
const chunks = [];
const pump = async () => {
while (true) {
const { value, done } = await reader.read();
if (done) { break; }
// 流式下载
fragment.fileStream ? fragment.fileStream.write(new Uint8Array(value)) : chunks.push(value);
receivedLength += value.length;
this.emit('itemProgress', fragment, false, receivedLength, contentLength, value);
}
if (fragment.fileStream) {
return new ArrayBuffer();
}
const allChunks = new Uint8Array(receivedLength);
let position = 0;
for (const chunk of chunks) {
allChunks.set(chunk, position);
position += chunk.length;
}
this.emit('itemProgress', fragment, true);
return allChunks.buffer;
}
return pump();
})
.then(buffer => {
this.emit('rawBuffer', buffer, fragment);
// 存在解密函数 调用解密函数 否则直接返回buffer
return this.decrypt ? this.decrypt(buffer, fragment) : buffer;
})
.then(buffer => {
this.emit('decryptedData', buffer, fragment);
// 存在转码函数 调用转码函数 否则直接返回buffer
return this.transcode ? this.transcode(buffer, fragment) : buffer;
})
.then(buffer => {
// 储存解密/转码后的buffer
this.buffer[fragment.index] = buffer;
// 成功数+1 累计buffer大小和视频时长
this.success++;
this.buffersize += buffer.byteLength;
this.duration += fragment.duration ?? 0;
// 下载对象来自错误列表 从错误列表内删除
this.errorList.has(fragment) && this.errorList.delete(fragment);
// 推送顺序下载
this.sequentialPush();
this.emit('completed', buffer, fragment);
// 下载完成
if (this.success == this.fragments.length) {
this.state = 'done';
this.emit('allCompleted', this.buffer, this.fragments);
}
}).catch((error) => {
console.log(error);
if (error.name == 'AbortError') {
this.emit('stop', fragment, error);
return;
}
this.emit('downloadError', fragment, error);
// 储存下载错误切片
!this.errorList.has(fragment) && this.errorList.add(fragment);
}).finally(() => {
this.running--;
// 下载下一个切片
if (!directDownload && this.index < this.fragments.length) {
this.downloader();
}
});
}
/**
* 开始下载 准备数据 调用下载器
* @param {number} start 下载范围 开始索引
* @param {number} end 下载范围 结束索引
*/
start(start = 0, end = this.fragments.length) {
// 检查下载器状态
if (this.state == 'running') {
this.emit('error', 'state running');
return;
}
// 从下载范围内 切出需要下载的部分
if (!this.range(start, end)) {
return;
}
// 初始化变量
this.init();
// 开始下载 多少线程开启多少个下载器
for (let i = 0; i < this.thread && i < this.fragments.length; i++) {
this.downloader();
}
}
/**
* 销毁 初始化所有变量
*/
destroy() {
this.stop();
this._fragments = [];
this.allFragments = [];
this.thread = 6;
this.events = {};
this.decrypt = null;
this.transcode = null;
this.init();
}
}

1941
js/m3u8.js Normal file

File diff suppressed because it is too large Load Diff

204
js/media-control.js Normal file
View File

@@ -0,0 +1,204 @@
(function () {
let _tabId = -1; // 选择的页面ID
let _index = -1; //选择的视频索引
let VideoTagTimer; // 获取所有视频标签的定时器
let VideoStateTimer; // 获取所有视频信息的定时器
let compareTab = [];
let compareVideo = [];
function setVideoTagTimer() {
clearInterval(VideoTagTimer);
VideoTagTimer = setInterval(getVideoTag, 1000);
}
function getVideoTag() {
chrome.tabs.query({ windowType: "normal" }, function (tabs) {
let videoTabList = [];
for (let tab of tabs) {
videoTabList.push(tab.id);
}
if (compareTab.toString() == videoTabList.toString()) {
return;
}
compareTab = videoTabList;
// 列出所有标签
for (let tab of tabs) {
if ($("#option" + tab.id).length == 1) { continue; }
$("#videoTabIndex").append(`<option value='${tab.id}' id="option${tab.id}">${stringModify(tab.title)}</option>`);
}
// 删除没有媒体的标签. 异步的原因使用一个for去处理无法保证标签顺序一致
for (let tab of videoTabList) {
chrome.tabs.sendMessage(tab, { Message: "getVideoState", index: 0 }, { frameId: 0 }, function (state) {
if (chrome.runtime.lastError || state.count == 0) {
$("#option" + tab).remove();
return;
}
$("#videoTabTips").remove();
if (tab == G.tabId && _tabId == -1) {
_tabId = tab;
$("#videoTabIndex").val(tab);
}
});
}
});
}
function setVideoStateTimer() {
clearInterval(VideoStateTimer);
VideoStateTimer = setInterval(getVideoState, 500);
}
function getVideoState(setSpeed = false) {
if (_tabId == -1) {
let currentTabId = $("#videoTabIndex").val();
if (currentTabId == -1) { return; }
_tabId = parseInt(currentTabId);
}
chrome.tabs.sendMessage(_tabId, { Message: "getVideoState", index: _index }, { frameId: 0 }, function (state) {
if (chrome.runtime.lastError || state.count == 0) { return; }
if (state.type == "audio") {
$("#pip").hide();
$("#screenshot").hide();
}
$("#volume").val(state.volume);
if (state.duration && state.duration != Infinity) {
$("#timeShow").html(secToTime(state.currentTime) + " / " + secToTime(state.duration));
$("#time").val(state.time);
}
state.paused ? $("#control").html(i18n.play).data("switch", "play") : $("#control").html(i18n.pause).data("switch", "pause");
state.speed == 1 ? $("#speed").html(i18n.speedPlayback).data("switch", "speed") : $("#speed").html(i18n.normalPlay).data("switch", "normal");
$("#loop").prop("checked", state.loop);
$("#muted").prop("checked", state.muted);
if (setSpeed && state.speed != 1) {
$("#playbackRate").val(state.speed);
}
if (compareVideo.toString() != state.src.toString()) {
compareVideo = state.src;
$("#videoIndex").empty();
for (let i = 0; i < state.count; i++) {
let src = state.src[i].split("/").pop();
if (src.length >= 60) {
src = src.substr(0, 35) + '...' + src.substr(-35);
}
$("#videoIndex").append(`<option value='${i}'>${src}</option>`);
}
}
_index = _index == -1 ? 0 : _index;
$("#videoIndex").val(_index);
});
}
// 点击其他设置标签页 开始读取tab信息以及视频信息
getVideoTag();
$("#otherTab").click(function () {
chrome.tabs.get(G.mediaControl.tabid, function (tab) {
if (chrome.runtime.lastError) {
_tabId = -1;
_index = -1;
setVideoTagTimer(); getVideoState(); setVideoStateTimer();
return;
}
chrome.tabs.sendMessage(G.mediaControl.tabid, { Message: "getVideoState", index: 0 }, function (state) {
_tabId = G.mediaControl.tabid;
if (state.count > G.mediaControl.index) {
_index = G.mediaControl.index;
}
$("#videoTabIndex").val(_tabId);
setVideoTagTimer(); getVideoState(true); setVideoStateTimer();
(chrome.storage.session ?? chrome.storage.local).set({ mediaControl: { tabid: _tabId, index: _index } });
});
});
// setVideoTagTimer(); getVideoState(); setVideoStateTimer();
});
// 切换标签选择 切换视频选择
$("#videoIndex, #videoTabIndex").change(function () {
if (!G.isFirefox) { $("#pip").show(); }
$("#screenshot").show();
if (this.id == "videoTabIndex") {
_tabId = parseInt($("#videoTabIndex").val());
} else {
_index = parseInt($("#videoIndex").val());
}
(chrome.storage.session ?? chrome.storage.local).set({ mediaControl: { tabid: _tabId, index: _index } });
getVideoState(true);
});
let wheelPlaybackRateTimeout;
$("#playbackRate").on("wheel", function (event) {
$(this).blur();
let speed = parseFloat($(this).val());
speed = event.originalEvent.wheelDelta < 0 ? speed - 0.1 : speed + 0.1;
speed = parseFloat(speed.toFixed(1));
if (speed < 0.1 || speed > 16) { return false; }
$(this).val(speed);
clearTimeout(wheelPlaybackRateTimeout);
wheelPlaybackRateTimeout = setTimeout(() => {
chrome.storage.sync.set({ playbackRate: speed });
chrome.tabs.sendMessage(_tabId, { Message: "speed", speed: speed, index: _index });
}, 200);
return false;
});
// 倍速播放
$("#speed").click(function () {
if (_index < 0 || _tabId < 0) { return; }
if ($(this).data("switch") == "speed") {
const speed = parseFloat($("#playbackRate").val());
chrome.tabs.sendMessage(_tabId, { Message: "speed", speed: speed, index: _index });
chrome.storage.sync.set({ playbackRate: speed });
return;
}
chrome.tabs.sendMessage(_tabId, { Message: "speed", speed: 1, index: _index });
});
// 画中画
$("#pip").click(function () {
if (_index < 0 || _tabId < 0) { return; }
chrome.tabs.sendMessage(_tabId, { Message: "pip", index: _index }, function (state) {
if (chrome.runtime.lastError) { return; }
state.state ? $("#pip").html(i18n.exit) : $("#pip").html(i18n.pictureInPicture);
});
});
// 全屏
$("#fullScreen").click(function () {
if (_index < 0 || _tabId < 0) { return; }
chrome.tabs.get(_tabId, function (tab) {
chrome.tabs.highlight({ 'tabs': tab.index }, function () {
chrome.tabs.sendMessage(_tabId, { Message: "fullScreen", index: _index }, function (state) {
close();
});
});
});
});
// 暂停 播放
$("#control").click(function () {
if (_index < 0 || _tabId < 0) { return; }
const action = $(this).data("switch");
chrome.tabs.sendMessage(_tabId, { Message: action, index: _index });
});
// 循环 静音
$("#loop, #muted").click(function () {
if (_index < 0 || _tabId < 0) { return; }
const action = $(this).prop("checked");
chrome.tabs.sendMessage(_tabId, { Message: this.id, action: action, index: _index });
});
// 调节音量和视频进度时 停止循环任务
$("#volume, #time").mousedown(function () {
if (_index < 0 || _tabId < 0) { return; }
clearInterval(VideoStateTimer);
});
// 调节音量
$("#volume").mouseup(function () {
if (_index < 0 || _tabId < 0) { return; }
chrome.tabs.sendMessage(_tabId, { Message: "setVolume", volume: $(this).val(), index: _index }, function () {
if (chrome.runtime.lastError) { return; }
setVideoStateTimer();
});
});
// 调节视频进度
$("#time").mouseup(function () {
if (_index < 0 || _tabId < 0) { return; }
chrome.tabs.sendMessage(_tabId, { Message: "setTime", time: $(this).val(), index: _index }, function () {
if (chrome.runtime.lastError) { return; }
setVideoStateTimer();
});
});
// 视频截图
$("#screenshot").click(function () {
if (_index < 0 || _tabId < 0) { return; }
chrome.tabs.sendMessage(_tabId, { Message: "screenshot", index: _index });
});
})();

187
js/mpd.js Normal file
View File

@@ -0,0 +1,187 @@
// url 参数解析
const params = new URL(location.href).searchParams;
const _url = params.get("url");
// const _referer = params.get("referer");
const _requestHeaders = params.get("requestHeaders");
const _title = params.get("title");
// 修改当前标签下的所有xhr的requestHeaders
let requestHeaders = JSONparse(_requestHeaders);
setRequestHeaders(requestHeaders, () => { awaitG(init); });
var mpdJson = {}; // 解析器json结果
var mpdXml = {}; // 解析器xml结果
// var mpdContent; // mpd文件内容
var m3u8Content = ""; //m3u8内容
var mediaInfo = "" // 媒体文件信息
chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
if (message == "getM3u8") {
sendResponse({ m3u8Content, mediaInfo });
}
});
function init() {
loadCSS();
if (_url) {
fetch(_url)
.then(response => response.text())
.then(function (text) {
// mpdContent = text;
// parseMPD(mpdContent);
parseMPD(text);
$("#mpd_url").html(_url).attr("href", _url);
});
} else {
$("#loading").hide();
$("#mpdCustom").show();
$("#parse").click(function () {
let url = $("#mpdUrl").val().trim();;
url = "mpd.html?url=" + encodeURIComponent(url);
let referer = $("#referer").val().trim();;
if (referer) { url += "&requestHeaders=" + JSON.stringify({ referer: referer }); }
chrome.tabs.update({ url: url });
});
}
$("#mpdVideoLists, #mpdAudioLists").change(function () {
let type = this.id == "mpdVideoLists" ? "video" : "audio";
showSegment(type, $(this).val());
});
$("#getVideo, #getAudio").click(function () {
let type = "video";
let index = $("#mpdVideoLists").val();
if (this.id == "getAudio") {
type = "audio";
index = $("#mpdAudioLists").val();
}
showSegment(type, index);
});
$("#videoToM3u8, #audioToM3u8").click(function () {
let index = $("#mpdVideoLists").val();
let items = mpdJson.playlists[index];
let type = "video";
if (this.id == "audioToM3u8") {
index = $("#mpdAudioLists").val();
let temp = index.split("$-bmmmd-$");
index = temp[0];
let index2 = temp[1];
items = mpdJson.mediaGroups.AUDIO.audio[index].playlists[index2];
type = "audio";
}
mediaInfo = getInfo(type);
m3u8Content = "#EXTM3U\n";
m3u8Content += "#EXT-X-VERSION:3\n";
m3u8Content += "#EXT-X-TARGETDURATION:" + items.targetDuration + "\n";
m3u8Content += "#EXT-X-MEDIA-SEQUENCE:0\n";
m3u8Content += "#EXT-X-PLAYLIST-TYPE:VOD\n";
m3u8Content += '#EXT-X-MAP:URI="' + items.segments[0].map.resolvedUri + '"\n';
for (let key in items.segments) {
m3u8Content += "#EXTINF:" + items.segments[key].duration + ",\n"
m3u8Content += items.segments[key].resolvedUri + "\n";
}
m3u8Content += "#EXT-X-ENDLIST";
// $("#media_file").html(m3u8Content); return;
chrome.tabs.getCurrent(function (tabs) {
chrome.tabs.create({ url: "m3u8.html?getId=" + tabs.id });
});
});
}
// 加密类型
function getEncryptionType(schemeIdUri) {
if (schemeIdUri.includes("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed")) {
return "Widevine";
} else if (schemeIdUri.includes("9a04f079-9840-4286-ab92-e65be0885f95")) {
return "Microsoft PlayReady";
} else if (schemeIdUri.includes("94ce86fb-07ff-4f43-adb8-93d2fa968ca2")) {
return "Apple FairPlay";
} else {
return "Unknown";
}
}
// 判断DRM
function isDRM(mpdContent) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(mpdContent, "application/xml");
let drmInfo = new Map();
const contentProtections = xmlDoc.getElementsByTagName("ContentProtection");
for (let i = 0; i < contentProtections.length; i++) {
const protection = contentProtections[i];
const schemeIdUri = protection.getAttribute("schemeIdUri");
let pssh = protection.getElementsByTagName("cenc:pssh")[0];
if (!pssh) {
pssh = protection.getElementsByTagName("mspr:pro")[0];
}
if (schemeIdUri && pssh) {
if (!drmInfo.has(schemeIdUri)) {
drmInfo.set(schemeIdUri, pssh.textContent);
}
}
}
return Array.from(drmInfo.entries()).map(([schemeIdUri, pssh]) => ({
schemeIdUri,
pssh,
encryptionType: getEncryptionType(schemeIdUri)
}));
}
function parseMPD(mpdContent) {
$("#loading").hide(); $("#main").show();
mpdJson = mpdParser.parse(mpdContent, { manifestUri: _url });
const drmInfo = isDRM(mpdContent);
if (drmInfo.length > 0) {
$("#loading").show();
$("#loading .optionBox").html(`<b>${i18n.DRMerror}</b><br><br>`);
drmInfo.map(item => {
$("#loading .optionBox").append(`<b>${item.encryptionType}</b><input value="${item.pssh}" style="width: 100%;"/>`);
});
}
for (let key in mpdJson.playlists) {
$("#mpdVideoLists").append(`<option value='${key}'>${mpdJson.playlists[key].attributes.NAME
} | ${(mpdJson.playlists[key].attributes.BANDWIDTH / 1024).toFixed(1)
} kbps | ${mpdJson.playlists[key].attributes["FRAME-RATE"].toFixed(1)
} fps | ${mpdJson.playlists[key].attributes.RESOLUTION.width
} x ${mpdJson.playlists[key].attributes.RESOLUTION.height
}</option>`);
}
for (let key in mpdJson.mediaGroups.AUDIO.audio) {
for (let index in mpdJson.mediaGroups.AUDIO.audio[key].playlists) {
let item = mpdJson.mediaGroups.AUDIO.audio[key].playlists[index];
// console.log(item);
$("#mpdAudioLists").append(`<option value='${key}$-bmmmd-$${index}'>${key} | ${item.attributes.NAME} | ${item.attributes.BANDWIDTH / 1000}Kbps</option>`);
}
}
$("#info").html(getInfo("video"));
showSegment("video", 0);
}
function showSegment(type, index) {
let textarea = "";
let items;
if (type == "video") {
items = mpdJson.playlists[index];
} else {
let temp = index.split("$-bmmmd-$");
index = temp[0];
let index2 = temp[1];
items = mpdJson.mediaGroups.AUDIO.audio[index].playlists[index2];
}
for (let key in items.segments) {
textarea += items.segments[key].resolvedUri + "\n\n";
}
$("#media_file").html(textarea);
$("#count").html(i18n("m3u8Info", [items.segments.length, secToTime(mpdJson.duration)]));
items.segments.length > 0 && $("#tips").html('initialization: <input type="text" value="' + items.segments[0].map.resolvedUri + '" spellcheck="false" readonly="readonly" class="width100">');
$("#info").html(getInfo(type));
}
function getInfo(type = "audio") {
if (type == "audio") {
return i18n.audio + ": " + $("#mpdAudioLists").find("option:selected").text();
} else {
return i18n.video + ": " + $("#mpdVideoLists").find("option:selected").text();
}
}

432
js/options.js Normal file
View File

@@ -0,0 +1,432 @@
////////////////////// 填充数据 //////////////////////
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];
}
}
if (items.Ext === undefined || items.Type === undefined || items.Regex === undefined) {
location.reload();
}
if (G.isMobile) {
$(`<link rel="stylesheet" type="text/css" href="css/mobile.css">`).appendTo("head");
}
$(`<style>${items.css}</style>`).appendTo("head");
const $extList = $("#extList");
for (let key in items.Ext) {
$extList.append(Gethtml("Ext", { ext: items.Ext[key].ext, size: items.Ext[key].size, state: items.Ext[key].state }));
}
const $typeList = $("#typeList");
for (let key in items.Type) {
$typeList.append(Gethtml("Type", { type: items.Type[key].type, size: items.Type[key].size, state: items.Type[key].state }));
}
const $regexList = $("#regexList");
for (let key in items.Regex) {
$regexList.append(Gethtml("Regex", { type: items.Regex[key].type, regex: items.Regex[key].regex, ext: items.Regex[key].ext, blackList: items.Regex[key].blackList, state: items.Regex[key].state }));
}
const $blockUrlList = $("#blockUrlList");
for (let key in items.blockUrl) {
$blockUrlList.append(Gethtml("blockUrl", { url: items.blockUrl[key].url, state: items.blockUrl[key].state }));
}
setTimeout(() => {
for (let key in items) {
if (key == "Ext" || key == "Type" || key == "Regex") { continue; }
if (typeof items[key] == "boolean") {
$(`#${key}`).prop("checked", items[key]);
} else {
$(`#${key}`).val(items[key]);
}
}
}, 100);
});
//新增格式
$("#AddExt").bind("click", function () {
$("#extList").append(Gethtml("Ext", { state: true }));
$("#extList [name=text]").last().focus();
});
$("#AddType").bind("click", function () {
$("#typeList").append(Gethtml("Type", { state: true }));
$("#typeList [name=text]").last().focus();
});
$("#AddRegex").bind("click", function () {
$("#regexList").append(Gethtml("Regex", { type: "ig", state: true }));
$("#regexList [name=text]").last().focus();
});
$("#blockAddUrl").bind("click", function () {
$("#blockUrlList").append(Gethtml("blockUrl", { state: true }));
$("#blockUrlList [name=url]").last().focus();
});
$("#version").html(i18n.catCatch + " v" + chrome.runtime.getManifest().version);
// 自定义播放调用模板
playerList = new Map();
playerList.set("tips", { name: i18n.invokeProtocolTemplate, template: "" });
playerList.set("default", { name: i18n.default + " / " + i18n.disable, template: "" });
playerList.set("potplayer", { name: "PotPlayer", template: "potplayer://${url} ${referer|exists:'/referer=\"*\"'}" });
playerList.set("potplayerFix", { name: "PotPlayerFix", template: "potplayer:${url} ${referer|exists:'/referer=\"*\"'}" });
playerList.set("mxPlayerAd", { name: "Android MX Player Free", template: "intent:${url}#Intent;package=com.mxtech.videoplayer.ad;end" });
playerList.set("mxPlayerPro", { name: "Android MX Player Pro", template: "intent:${url}#Intent;package=com.mxtech.videoplayer.pro;end" });
playerList.set("vlc", { name: "Android vlc", template: "intent:${url}#Intent;package=org.videolan.vlc;end" });
playerList.set("vlcCustom", { name: i18n.customVLCProtocol + " vlc://", template: "vlc://${url}" });
playerList.set("shareApi", { name: i18n.systemShare, template: "${shareApi}" });
playerList.forEach(function (item, key) {
$("#PlayerTemplate").append(`<option value="${key}">${item.name}</option>`);
});
// 增加后缀 类型 正则表达式
function Gethtml(Type, Param = new Object()) {
let html = "";
switch (Type) {
case "Ext":
html = `<td><input type="text" value="${Param.ext ? Param.ext : ""}" name="text" placeholder="${i18n.suffix}" class="ext"></td>`
html += `<td><input type="number" value="${Param.size ? Param.size : 0}" class="size" name="size">KB</td>`
break;
case "Type":
html = `<td><input type="text" value="${Param.type ? Param.type : ""}" name="text" placeholder="${i18n.type}" class="type"></td>`
html += `<td><input type="number" value="${Param.size ? Param.size : 0}" class="size" name="size">KB</td>`
break;
case "Regex":
html = `<td><input type="text" value="${Param.type ? Param.type : ""}" name="type" class="regexType"></td>`
html += `<td><input type="text" value="${Param.regex ? Param.regex : ""}" placeholder="${i18n.regexExpression}" name="regex" class="regex"></td>`
html += `<td><input type="text" value="${Param.ext ? Param.ext : ""}" name="regexExt" class="regexExt"></td>`
html += `<td>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" name="blackList" class="switchInput" ${Param.blackList ? 'checked="checked"' : ""}/>
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</td>`
break;
case "blockUrl":
html = `<td><input type="text" value="${Param.url ? Param.url : ""}" name="url" placeholder="${i18n.blockUrlTips}" class="width100"></td>`
break;
}
html = $(`<tr data-type="${Type}">
${html}
<td>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" name="state" class="switchInput" ${Param.state ? 'checked="checked"' : ""}/>
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</td>
<td>
<img src="img/delete.svg" class="RemoveButton">
</td>
</tr>`);
html.find(".RemoveButton").click(function () {
html.remove();
Save(Type);
});
html.find("input").on("input", function () {
Save(Type, 200);
});
html.find("[name=state]").on("click", function () {
Save(Type);
});
if (Type == "Type") {
html.find("input").blur(function () {
$("#typeList tr").each(function () {
let GetText = $(this).find("[name=text]").val();
if (isEmpty(GetText)) { return true; }
GetText = GetText.trim();
const test = GetText.split("/");
if (test.length != 2 || isEmpty(test[0]) || isEmpty(test[1])) {
alert(i18n.addTypeError);
return true;
}
});
});
}
return html;
}
// 预览模板
$("#PlayerTemplate").change(function () {
const Value = $(this).val();
if (this.id == "PlayerTemplate" && playerList.has(Value) && Value != "tips") {
const template = playerList.get(Value).template;
$("#Player").val(template);
chrome.storage.sync.set({ Player: template });
}
});
//失去焦点 保存自动清理数 模拟手机User Agent 自定义播放调用模板
let debounce2 = undefined;
$("[save='input']").on("input", function () {
let val = $(this).val().trim();
if (this.type == "number") {
val = parseInt(val);
}
clearTimeout(debounce2);
debounce2 = setTimeout(() => {
chrome.storage.sync.set({ [this.id]: val });
}, 300);
});
// 调试模式 使用网页标题做文件名 使用PotPlayer预览 显示网站图标 刷新自动清理
$("[save='click']").bind("click", function () {
chrome.storage.sync.set({ [this.id]: $(this).prop('checked') });
});
// [save='select'] 元素 储存
$("[save='select']").on("change", function () {
let val = $(this).val();
if (!isNaN(val)) { val = parseInt(val); }
chrome.storage.sync.set({ [this.id]: val });
});
// 一键禁用/启用
$("#allDisable, #allEnable").bind("click", function () {
const state = this.id == "allEnable";
const obj = $(this).data("switch");
let query;
if (obj == "Ext") {
query = $("#extList [name=state]");
} else if (obj == "Type") {
query = $("#typeList [name=state]");
} else if (obj == "Regex") {
query = $("#regexList [name=state]");
} else if (obj == "blockUrl") {
query = $("#blockUrlList [name=state]");
}
query.each(function () {
$(this).prop("checked", state);
});
Save(obj);
});
// m3u8dlArg 输出测试
function testTag() {
const data = {
url: $("#url").val(),
requestHeaders: { referer: $("#referer").val() },
initiator: $("#initiator").val(),
webUrl: $("#webUrl").val(),
title: $("#title").val(),
}
const result = templates($("#testTextarea").val() ?? "", data);
const m3u8dl = 'm3u8dl:' + (G.m3u8dl == 1 ? Base64.encode(result) : result);
$("#tagTestResult").html(`${result}<br><br><a href="${m3u8dl}" class="test_url">${m3u8dl}</a>`);
}
$("#showTestTag").bind("click", function () {
testTag();
$("#testTag").slideToggle();
});
$("#testTag input, #testTextarea").on("input", function () {
testTag();
});
//重置后缀 重置类型 重置正则
$("[data-reset]").bind("click", function () {
if (confirm(i18n.confirmReset)) {
const Option = $(this).data("reset");
chrome.storage.sync.set({ [Option]: G.OptionLists[Option] }, () => {
location.reload();
});
}
});
//重置设置
$(".resetOption").click(function () {
if (confirm(i18n.confirmReset)) {
const optionBox = $(this).closest('.optionBox');
const result = optionBox.find('[save]').toArray().reduce((acc, { id }) => {
acc[id] = G.OptionLists[id];
return acc;
}, {});
chrome.storage.sync.set(result, () => {
location.reload();
});
}
});
//清空数据 重置所有设置
$("#ClearData, #ResetAllOption").bind("click", function () {
if (this.id == "ResetAllOption") {
if (confirm(i18n.confirmReset)) {
chrome.storage.sync.clear();
InitOptions();
} else {
return;
}
}
chrome.storage.local.clear();
chrome.storage.session.clear();
chrome.runtime.sendMessage({ Message: "ClearIcon" });
location.reload();
});
//重启扩展
$("#extensionReload").bind("click", function () {
chrome.runtime.reload();
});
//正则表达式 测试
$("#testRegex, #testUrl").keyup(function () {
const testUrl = $("#testUrl").val();
const testRegex = $("#testRegex").val();
const testFlag = $("#testFlag").val();
if (testUrl == "" || testRegex == "") {
$("#testResult").html(i18n.noMatch);
return;
}
let regex;
try {
regex = new RegExp(testRegex, testFlag);
} catch (e) {
$("#testResult").html(e.message);
return;
}
const result = regex.exec(testUrl);
if (result == null) {
$("#testResult").html(i18n.noMatch);
return;
}
$("#testResult").html(i18n.match)
for (let i = 1; i < result.length; i++) {
if (result[i] != "") {
$("#testResult").append(
`<input type="text" style="width: 590px; color: #ff0000" value="${decodeURIComponent(result[i])}">`
);
}
}
});
//导出配置
$("#exportOptions").bind("click", function () {
chrome.storage.sync.get(null, function (items) {
let ExportData = JSON.stringify(items);
ExportData = "data:text/plain," + Base64.encode(ExportData);
let date = new Date();
const filename = `cat-catch-${chrome.runtime.getManifest().version}-${date.getFullYear()}${appendZero(date.getMonth() + 1)}${appendZero(date.getDate())}T${appendZero(date.getHours())}${appendZero(date.getMinutes())}.txt`;
if (G.isFirefox) {
downloadDataURL(ExportData, filename);
return;
}
chrome.downloads.download({
url: ExportData,
filename: filename
});
});
});
//导入配置
$("#importOptionsFile").change(function () {
const fileReader = new FileReader();
fileReader.onload = function () {
let importData = this.result;
try {
importData = JSON.parse(importData);
} catch (e) {
importData = Base64.decode(importData);
importData = JSON.parse(importData);
}
const keys = Object.keys(G.OptionLists);
for (let item in G.OptionLists) {
if (keys.includes(item) && importData[item]) {
chrome.storage.sync.set({ [item]: importData[item] });
}
}
alert("导入完成");
location.reload();
}
const file = $("#importOptionsFile").prop('files')[0];
fileReader.readAsText(file);
});
$("#importOptions").bind("click", function () {
$("#importOptionsFile").click();
});
// 保存 后缀 类型 正则 配置
function Save(option, sec = 0) {
clearTimeout(debounce);
debounce = setTimeout(() => {
if (option == "Ext") {
let Ext = new Array();
$("#extList tr").each(function () {
const _this = $(this);
let GetText = _this.find("[name=text]").val();
let GetSize = parseInt(_this.find("[name=size]").val());
let GetState = _this.find("[name=state]").prop("checked");
if (isEmpty(GetText)) { return true; }
if (isEmpty(GetSize)) { GetSize = 0; }
Ext.push({ ext: GetText.toLowerCase(), size: GetSize, state: GetState });
});
chrome.storage.sync.set({ Ext: Ext });
return;
}
if (option == "Type") {
let Type = new Array();
$("#typeList tr").each(function () {
const _this = $(this);
let GetText = _this.find("[name=text]").val();
let GetSize = parseInt(_this.find("[name=size]").val());
let GetState = _this.find("[name=state]").prop("checked");
if (isEmpty(GetText)) { return true; }
if (isEmpty(GetSize)) { GetSize = 0; }
GetText = GetText.trim();
const test = GetText.split("/");
if (test.length == 2 && !isEmpty(test[0]) && !isEmpty(test[1])) {
Type.push({ type: GetText.toLowerCase(), size: GetSize, state: GetState });
}
});
chrome.storage.sync.set({ Type: Type });
return;
}
if (option == "Regex") {
let Regex = new Array();
$("#regexList tr").each(function () {
const _this = $(this);
let GetType = _this.find("[name=type]").val();
let GetRegex = _this.find("[name=regex]").val();
let GetExt = _this.find("[name=regexExt]").val()
let GetState = _this.find("[name=state]").prop("checked");
let GetBlackList = _this.find("[name=blackList]").prop("checked");
try {
new RegExp("", GetType);
} catch (e) {
GetType = "ig";
}
if (isEmpty(GetRegex)) { return true; }
GetExt = GetExt ? GetExt.toLowerCase() : "";
Regex.push({ type: GetType, regex: GetRegex, ext: GetExt, blackList: GetBlackList, state: GetState });
});
chrome.storage.sync.set({ Regex: Regex });
return;
}
if (option == "blockUrl") {
let blockUrl = new Array();
$("#blockUrlList tr").each(function () {
const _this = $(this);
let url = _this.find("[name=url]").val();
let GetState = _this.find("[name=state]").prop("checked");
if (isEmpty(url)) { return true; }
blockUrl.push({ url: url, state: GetState });
});
chrome.storage.sync.set({ blockUrl: blockUrl });
return;
}
}, sec);
}
// 导航栏
document.querySelectorAll('nav a').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const targetId = this.getAttribute('href');
const targetElement = document.querySelector(targetId);
if (targetElement) {
targetElement.scrollIntoView({
behavior: 'smooth'
});
}
});
});
const adjustSidebarPosition = () => {
const wrapper = document.querySelector('.wrapper');
const sidebar = document.querySelector('.sidebar');
if (wrapper && sidebar) {
sidebar.style.left = `${wrapper.getBoundingClientRect().left - sidebar.offsetWidth - 20}px`;
}
}
window.addEventListener('load', adjustSidebarPosition)
window.addEventListener('resize', adjustSidebarPosition);

1065
js/popup.js Normal file

File diff suppressed because it is too large Load Diff

1035
js/preview.js Normal file

File diff suppressed because it is too large Load Diff

232
js/pupup-utils.js Normal file
View File

@@ -0,0 +1,232 @@
// 复制选项
function copyLink(data) {
let text = data.url;
if (data.parsing == "m3u8") {
text = G.copyM3U8;
} else if (data.parsing == "mpd") {
text = G.copyMPD;
} else {
text = G.copyOther;
}
return templates(text, data);
}
function isM3U8(data) {
return (
data.ext == "m3u8" ||
data.ext == "m3u" ||
data.type?.endsWith("/vnd.apple.mpegurl") ||
data.type?.endsWith("/x-mpegurl") ||
data.type?.endsWith("/mpegurl") ||
data.type?.endsWith("/octet-stream-m3u8")
)
}
function isMPD(data) {
return (data.ext == "mpd" ||
data.type == "application/dash+xml"
)
}
function isJSON(data) {
return (data.ext == "json" ||
data.type == "application/json" ||
data.type == "text/json"
)
}
function isPicture(data) {
return (data.type?.startsWith("image/") ||
data.ext == "jpg" ||
data.ext == "png" ||
data.ext == "jpeg" ||
data.ext == "bmp" ||
data.ext == "gif" ||
data.ext == "webp" ||
data.ext == "svg"
)
}
function isMediaExt(ext) {
return ['ogg', 'ogv', 'mp4', 'webm', 'mp3', 'wav', 'm4a', '3gp', 'mpeg', 'mov', 'm4s', 'aac'].includes(ext);
}
function isMedia(data) {
return isMediaExt(data.ext) || data.type?.startsWith("video/") || data.type?.startsWith("audio/");
}
/**
* ari2a RPC发送一套资源
* @param {object} data 资源对象
* @param {Function} success 成功运行函数
* @param {Function} error 失败运行函数
*/
function aria2AddUri(data, success, error) {
const json = {
"jsonrpc": "2.0",
"id": "cat-catch-" + data.requestId,
"method": "aria2.addUri",
"params": []
};
if (G.aria2RpcToken) {
json.params.push(`token:${G.aria2RpcToken}`);
}
const params = { out: data.downFileName };
if (G.enableAria2RpcReferer) {
params.header = [];
params.header.push(G.userAgent ? G.userAgent : navigator.userAgent);
if (data.requestHeaders?.referer) {
params.header.push("Referer: " + data.requestHeaders.referer);
}
if (data.cookie) {
params.header.push("Cookie: " + data.cookie);
}
if (data.requestHeaders?.authorization) {
params.header.push("Authorization: " + data.requestHeaders.authorization);
}
}
json.params.push([data.url], params);
fetch(G.aria2Rpc, {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8"
},
body: JSON.stringify(json)
}).then(response => {
return response.json();
}).then(data => {
success && success(data);
}).catch(errMsg => {
error && error(errMsg);
});
}
// MQTT 相关功能
/**
* 发送数据到 MQTT 服务器
* @param {Object} data - 要发送的媒体数据
* @returns {Promise} - 返回发送结果的 Promise
*/
function sendToMQTT(data, config) {
return new Promise((resolve, reject) => {
if (!G.mqttEnable) {
reject("MQTT is not enabled");
return;
}
// 使用配置的标题长度如果未设置则默认为100
const titleLength = G.mqttTitleLength || 100;
data.title = data.title.slice(0, titleLength) || "";
data.action = "media_found";
data = trimData(data);
// 创建 MQTT 连接并发送数据
connectAndSendMQTT(data, config)
.then(() => {
resolve(true);
})
.catch((error) => {
console.error("MQTT send error:", error);
reject("MQTT send failed: " + error.message);
});
});
}
/**
* 连接到 MQTT 服务器并发送消息
* @param {Object} data - 要发送的数据
* @returns {Promise} - 连接和发送的 Promise
*/
function connectAndSendMQTT(data, config) {
return new Promise((resolve, reject) => {
try {
// 构建 MQTT 连接 URL
const protocol = G.mqttProtocol;
const broker = G.mqttBroker;
const port = G.mqttPort;
const path = G.mqttPath;
if (!protocol || !broker || !port || !path) {
throw new Error("MQTT connection parameters are missing");
}
const mqttUrl = `${protocol}://${broker}:${port}${path}`;
// 创建 MQTT 客户端选项
const options = {
clientId: `${G.mqttClientId || "cat-catch-client"}-${Math.random().toString(16).slice(2)}`,
clean: true,
connectTimeout: 10000,
reconnectPeriod: 0 // 不自动重连,用完即断
};
// 添加用户名和密码(如果有)
if (G.mqttUser) {
options.username = G.mqttUser;
}
if (G.mqttPassword) {
options.password = G.mqttPassword;
}
const mqttLib = window.mqtt || (typeof mqtt !== 'undefined' ? mqtt : null);
if (!mqttLib) {
throw new Error("MQTT library not found. Please check if lib/mqtt.min.js is loaded correctly.");
}
if (!mqttLib.connect) {
throw new Error("MQTT.connect method not found. Available methods: " + Object.keys(mqttLib));
}
// 如果提供了提示回调函数,则使用它
if (typeof config?.alert === 'function') {
Tips = config.alert;
}
// 2. 创建连接阶段提示:正在连接 MQTT 服务器
Tips(i18n.connectingToMQTT || "Connecting to MQTT server...", 2000);
const client = mqttLib.connect(mqttUrl, options);
// 连接成功
client.on('connect', () => {
const topic = G.mqttTopic || "cat-catch/media";
const qos = parseInt(G.mqttQos) || 2;
// 处理自定义数据格式
let message;
if (G.mqttDataFormat?.trim()) {
message = templates(G.mqttDataFormat, data);
} else {
// 使用默认JSON格式
message = JSON.stringify(data);
}
// 3. 正在发送消息到 MQTT 服务器
Tips(i18n.sendingMessageToMQTT || "Sending message to MQTT server...", 2000);
// 发送消息
client.publish(topic, message, { qos: qos }, (error) => {
if (error) {
console.error("MQTT publish error:", error);
reject(error);
} else {
resolve();
}
});
});
// 连接错误
client.on('error', (error) => {
console.error("MQTT connection error:", error);
reject(error);
});
// 连接超时
setTimeout(() => {
if (!client.connected) {
client.end();
reject(new Error("MQTT connection timeout"));
}
}, 6000);
// client.on('close', () => {
// console.log('MQTT connection closed');
// });
} catch (error) {
console.error("MQTT setup error:", error);
reject(error);
}
});
}