// ==UserScript== // @name 蜜柑计划批量复制磁链 // @description 蜜柑计划批量复制磁链, 并保存为 md 文件. // @version 1.0.0 // @author Yiero // @match https://mikanani.me/Home/Bangumi/* // @license GPL-3 // @namespace https://github.com/AliubYiero/TamperMonkeyScripts // @grant GM_download // ==/UserScript== /* * @module : @yiero/gmlib * @author : Yiero * @version : 0.1.12 * @description : GM Lib for Tampermonkey * @keywords : tampermonkey, lib, scriptcat, utils * @license : MIT * @repository : git+https://github.com/AliubYiero/GmLib.git */ const returnElement = (selector, options, resolve, reject) => { setTimeout(() => { const element = options.parent.querySelector(selector); if (!element) { reject(new Error("Void Element")); return; } resolve(element); }, options.delayPerSecond * 1e3); }; const getElementByTimer = (selector, options, resolve, reject) => { const intervalDelay = 100; let intervalCounter = 0; const maxIntervalCounter = Math.ceil(options.timeoutPerSecond * 1e3 / intervalDelay); const timer = window.setInterval(() => { if (++intervalCounter > maxIntervalCounter) { clearInterval(timer); returnElement(selector, options, resolve, reject); return; } const element = options.parent.querySelector(selector); if (element) { clearInterval(timer); returnElement(selector, options, resolve, reject); } }, intervalDelay); }; const getElementByMutationObserver = (selector, options, resolve, reject) => { const timer = options.timeoutPerSecond && window.setTimeout(() => { observer.disconnect(); returnElement(selector, options, resolve, reject); }, options.timeoutPerSecond * 1e3); const observeElementCallback = (mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((addNode) => { if (addNode.nodeType !== Node.ELEMENT_NODE) { return; } const addedElement = addNode; const element = addedElement.matches(selector) ? addedElement : addedElement.querySelector(selector); if (element) { timer && clearTimeout(timer); returnElement(selector, options, resolve, reject); } }); }); }; const observer = new MutationObserver(observeElementCallback); observer.observe(options.parent, { subtree: true, childList: true }); return true; }; function elementWaiter(selector, options) { const elementWaiterOptions = { parent: document, timeoutPerSecond: 20, delayPerSecond: 0.5, ...options }; return new Promise((resolve, reject) => { const targetElement = elementWaiterOptions.parent.querySelector(selector); if (targetElement) { returnElement(selector, elementWaiterOptions, resolve, reject); return; } if (MutationObserver) { getElementByMutationObserver(selector, elementWaiterOptions, resolve, reject); return; } getElementByTimer(selector, elementWaiterOptions, resolve, reject); }); } const registerButton = (clickCallback) => { const subGroupNodeList = document.querySelectorAll(".subgroup-text"); const appendButton = (subGroupNode) => { const downloadLink = document.createElement("a"); downloadLink.classList.add("download-link", "subgroup-subscribe"); downloadLink.textContent = "\u4E0B\u8F7D\u6240\u6709\u94FE\u63A5"; downloadLink.addEventListener("click", (e) => { clickCallback(subGroupNode, e); }); subGroupNode.appendChild(downloadLink); }; subGroupNodeList.forEach((subGroupNode) => { appendButton(subGroupNode); }); }; const downloadTextFile = (fileName, fileContent, mimeType = "plain/text") => { const textBlob = new Blob([fileContent], { type: mimeType }); const textUrl = URL.createObjectURL(textBlob); GM_download({ url: textUrl, name: fileName, onload() { URL.revokeObjectURL(textUrl); } }); }; const parseAnimationData = (magnetContainer) => { const animationItemList = Array.from(magnetContainer.querySelectorAll("tbody > tr")); const animationInfoList = animationItemList.map((trNode) => { var _a; const [fileNameNode, fileSizeNode, updateTimeNode] = trNode.querySelectorAll("td"); if (!fileNameNode || !fileSizeNode || !updateTimeNode) return void 0; const fileName = ((_a = fileNameNode.querySelector("a.magnet-link-wrap")) == null ? void 0 : _a.textContent) || ""; const upperFileName = fileName.toUpperCase(); const language = upperFileName.includes("GB") ? "GB" : upperFileName.includes("BIG5") ? "BIG5" : "UNKNOWN"; const quality = upperFileName.includes("720P") ? "720P" : upperFileName.includes("1080P") ? "1080P" : "UNKNOWN"; const magnetLinkNode = fileNameNode.querySelector("a.magnet-link"); if (!magnetLinkNode) return void 0; const magnetLink = magnetLinkNode.dataset.clipboardText || ""; return { fileName, magnetLink, language, quality, fileSize: fileSizeNode.textContent || "", updateTime: updateTimeNode.textContent || "" }; }); const sortedAnimationInfoList = animationInfoList.toSorted((a, b) => { const parseUpdateTimeWeight = (updateTime) => { const UPDATE_TIME_WEIGHT = 1e12; return Date.parse(updateTime) / UPDATE_TIME_WEIGHT; }; const parseLanguageWeight = (language) => { const LANGUAGE_WEIGHT = 1e3; const languageWeightMapper = { "GB": 1, "BIG5": 2, "UNKNOWN": 3 }; return languageWeightMapper[language] * LANGUAGE_WEIGHT; }; const parseQualityWeight = (quality) => { const QUALITY_WEIGHT = 10; const qualityWeightMapper = { "1080P": 1, "720P": 2, "UNKNOWN": 3 }; return qualityWeightMapper[quality] * QUALITY_WEIGHT; }; const aWeight = parseLanguageWeight(a.language) + parseQualityWeight(a.quality) + parseUpdateTimeWeight(a.updateTime); const bWeight = parseLanguageWeight(b.language) + parseQualityWeight(b.quality) + parseQualityWeight(b.quality); return aWeight - bWeight; }); return sortedAnimationInfoList; }; const stringifyAnimationData = (animationInfoList) => { const groupByAnimationInfo = Object.groupBy(animationInfoList, (animationInfo) => { return `${animationInfo.language}-${animationInfo.quality}`; }); return Object.entries(groupByAnimationInfo).map(([title, infoList]) => { return ` ## ${title} | \u756A\u5267\u540D | \u5927\u5C0F | \u66F4\u65B0\u65F6\u95F4 | \u4E0B\u8F7D\u94FE\u63A5 | | ----- | ---- | ------ | ------- | ${infoList.map((info) => `| \`${info.fileName}\` | ${info.fileSize} | ${info.updateTime} | [#](${info.magnetLink}) |`).join("\n")} \`\`\`plain ${infoList.map((info) => info.magnetLink).join("\n")} \`\`\` `; }).join("\n\n---\n\n"); }; (async () => { var _a; await elementWaiter("footer.footer"); const animationName = (_a = document.querySelector(".bangumi-title")) == null ? void 0 : _a.textContent; if (!animationName) return; document.querySelectorAll('a.episode-expand.js-expand-episode:not([style="display: none;"])').forEach((item) => item.click()); registerButton((subGroupNode) => { var _a2; const subGroupNameNode = subGroupNode.firstElementChild; if (!subGroupNameNode) return; const subGroupName = subGroupNameNode.classList.contains("dropdown") ? (((_a2 = subGroupNameNode.querySelector(".dropdown-toggle")) == null ? void 0 : _a2.textContent) || "").trim() : (subGroupNameNode.textContent || "").trim(); if (!subGroupName) return; const magnetContainer = subGroupNode.nextElementSibling; if (!(magnetContainer && magnetContainer.tagName === "TABLE")) return; const animationInfoList = parseAnimationData(magnetContainer); const animationDataHeader = `# ${animationName} > - \u5B57\u5E55\u7EC4: ${subGroupName} > - \u756A\u5267\u94FE\u63A5: ${document.URL}`; const animationDataContent = stringifyAnimationData(animationInfoList); downloadTextFile(`${animationName.trim()}-${subGroupName.trim()}.md`, `${animationDataHeader} ${animationDataContent}`, "text/markdown"); }); })();