1
This commit is contained in:
869
js/background.js
Normal file
869
js/background.js
Normal 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
274
js/content-script.js
Normal 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
421
js/downloader.js
Normal 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
22
js/firefox.js
Normal 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
736
js/function.js
Normal 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("\\", "\").replaceAll("/", "/");
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换掉文件名中的特殊字符 不包含路径
|
||||
* @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 || {
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
':': ':',
|
||||
'"': '"',
|
||||
'|': '|',
|
||||
'?': '?',
|
||||
'*': '*',
|
||||
'~': '_'
|
||||
}[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
15
js/i18n.js
Normal 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
395
js/init.js
Normal 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
64
js/install.js
Normal 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
85
js/json.js
Normal 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
333
js/m3u8.downloader.js
Normal 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
1941
js/m3u8.js
Normal file
File diff suppressed because it is too large
Load Diff
204
js/media-control.js
Normal file
204
js/media-control.js
Normal 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
187
js/mpd.js
Normal 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
432
js/options.js
Normal 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
1065
js/popup.js
Normal file
File diff suppressed because it is too large
Load Diff
1035
js/preview.js
Normal file
1035
js/preview.js
Normal file
File diff suppressed because it is too large
Load Diff
232
js/pupup-utils.js
Normal file
232
js/pupup-utils.js
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user