From 6c9a4743bbff7f11c85d855ff866268b17d7aa6d Mon Sep 17 00:00:00 2001 From: "lzyme.dev@gmail.com" Date: Mon, 31 Aug 2026 13:50:35 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=AA=8C=E8=BD=A6=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dist/jhs-enhance.user.js | 489 ++++++++++++++++++++------------------ src/plugins/magnet-hub.js | 381 +++++++++++++++-------------- 2 files changed, 459 insertions(+), 411 deletions(-) diff --git a/dist/jhs-enhance.user.js b/dist/jhs-enhance.user.js index e87f1a9..76de1b5 100644 --- a/dist/jhs-enhance.user.js +++ b/dist/jhs-enhance.user.js @@ -11137,44 +11137,50 @@ ${err.stack}` : ""); constructor() { super(...arguments); __publicField2(this, "currentEngine", null); - __publicField2(this, "searchEngines", [{ - name: "U9A9", - id: "u9a9", - url: "https://u9a9.com/?type=2&search={keyword}", - targetPage: "https://u9a9.com/?type=2&search={keyword}", + __publicField2(this, "searchEngines", [ + { + name: "Sukebei", + id: "Sukebei", + url: "https://sukebei.nyaa.si/?f=0&c=0_0&q={keyword}", + targetPage: "https://sukebei.nyaa.si/?f=0&c=0_0&q={keyword}", + parseHtml: this.parseSukebei + }, + { + name: "U9A9", + id: "u9a9", + url: "https://u9a9.com/?type=2&search={keyword}", + targetPage: "https://u9a9.com/?type=2&search={keyword}", parseHtml: this.parseU9A9 - }, { - name: "U3C3", - id: "u3c3", - url: "https://u3c3.com/?search2={search2}&search={keyword}", - targetPage: "https://u3c3.com/?search2={search2}&search={keyword}", - parseHtml: this.parseU3C3, - dynamicSearch2: true - }, { - name: "Sukebei", - id: "Sukebei", - url: "https://sukebei.nyaa.si/?f=0&c=0_0&q={keyword}", - targetPage: "https://sukebei.nyaa.si/?f=0&c=0_0&q={keyword}", - parseHtml: this.parseSukebei - }, { - name: "ØMagnet", - id: "omag", - url: "https://xn--mag-zna.net/search?q={keyword}", - targetPage: "https://xn--mag-zna.net/search?q={keyword}", - parseHtml: this.parseOmag - }, { + }, + { + name: "U3C3", + id: "u3c3", + url: "https://u3c3.com/?search={keyword}", + targetPage: "https://u3c3.com/?search={keyword}", + parseHtml: this.parseU3C3 + }, + { name: "BTSearch", id: "btsearch", url: "https://btsearch.love/api/search?keyword={keyword}&limit=10&offset=0&sort=size&sort_type=desc", targetPage: "https://btsearch.love/search?keyword={keyword}", parseJson: this.parseBTSearch - }, { + }, + { name: "SoKitty", id: "sokitty", url: "https://w1.sokitty.me/search?key={keyword}", targetPage: "https://w1.sokitty.me/search?key={keyword}", parseHtml: this.parseSokitty - }]); + }, + { + name: "ØMagnet", + id: "omag", + url: "https://xn--mag-zna.net/search?q={keyword}", + targetPage: "https://xn--mag-zna.net/search?q={keyword}", + parseHtml: this.parseOmag + } + ]); } getName() { return "MagnetHubPlugin"; @@ -11224,7 +11230,7 @@ ${err.stack}` : ""); border-bottom: 1px solid #eee; } .magnet-sort-select { - height: 26px; + height: 28px; padding: 2px 20px 2px 6px; border: 1px solid #ccc; border-radius: 4px; @@ -11255,9 +11261,10 @@ ${err.stack}` : ""); font-size: 12px; text-align: left; } - .magnet-table th.col-size { width: 72px; text-align: center; } + .magnet-table th.col-name { max-width: 0; } + .magnet-table th.col-size { width: 70px; text-align: center; } .magnet-table th.col-date { width: 90px; text-align: center; } - .magnet-table th.col-actions { width: 130px; text-align: center; } + .magnet-table th.col-actions { width: 180px; text-align: center; } .magnet-table td { padding: 8px 6px; border-bottom: 1px solid #eee; @@ -11286,6 +11293,7 @@ ${err.stack}` : ""); color: #243b67; font-weight: 650; text-decoration: none; + line-height: 1; } .magnet-name-cell > a:hover { color: #1d4ed8; @@ -11299,12 +11307,12 @@ ${err.stack}` : ""); border-radius: 3px; font-size: 11px; font-weight: 800; - line-height: 1.4; + line-height: 1; color: #fff; } - .magnet-badge.subtitle { background: #16a34a; } - .magnet-badge.four-k { background: #2563eb; } - .magnet-badge.cracked { background: #be123c; } + .magnet-badge.mg-subtitle { background: #16a34a; } + .magnet-badge.mg-four-k { background: #2563eb; } + .magnet-badge.mg-cracked { background: #be123c; } .magnet-loading { text-align: center; padding: 20px; @@ -11328,7 +11336,7 @@ ${err.stack}` : ""); background-color: #f0f0f0; color: #555; border: 1px solid #ddd; - padding: 3px 8px; + padding: 5px 10px; border-radius: 3px; cursor: pointer; font-size: 12px; @@ -11374,7 +11382,7 @@ ${err.stack}` : ""); .whatslink-overlay { position: fixed; inset: 0; - z-index: 10000040; + z-index: 2147483700; display: flex; align-items: center; justify-content: center; @@ -11532,31 +11540,29 @@ ${err.stack}` : ""); const key = "jhs_magnetHub_selectedEngine"; const savedEngineId = localStorage.getItem(key); let defaultEngineIndex = 0; - const $tabBox = $('
'); + const $tabBox = $('
'); this.searchEngines.forEach(((engine, index) => { const $tab = $(`
${engine.name}
`); if (savedEngineId && engine.id === savedEngineId) { $tab.addClass("active"); this.currentEngine = engine; defaultEngineIndex = index; - } else if (0 === index && !savedEngineId) { + } else if (!savedEngineId && engine.id === "Sukebei") { $tab.addClass("active"); this.currentEngine = engine; } $tabBox.append($tab); })); - $tabs.append($tabBox); - $tabs.append(`原网页`); - $container.append($tabs); - const $controls = $('
'); const sortMode = localStorage.getItem("jhs_magnetSort") || "size"; - const $sortSelect = $(` `); - $controls.append($sortSelect); - $container.append($controls); + $tabBox.append($sortSelect); + $tabs.append($tabBox); + $tabs.append(`原网页`); + $container.append($tabs); const $resultsContainer = $('
'); $container.append($resultsContainer); $container.on("click", ".magnet-tab", ((e) => { @@ -11615,17 +11621,13 @@ ${err.stack}` : ""); sessionStorage.getItem(cacheKey); this.resolveUrl(engine, keyword).then(((url) => { const requestOpts = {method: "GET", url}; - if (engine.dynamicSearch2) { - const cookie = sessionStorage.getItem("jhs_u3c3_cookie"); - if (cookie) requestOpts.cookie = cookie; - } if (engine.parseHtml) { _GM_xmlhttpRequest(Object.assign(requestOpts, { method: "GET", url, - onload: (response) => { + onload: async (response) => { try { - const results = engine.parseHtml.call(this, response.responseText, keyword); + const results = await engine.parseHtml.call(this, response.responseText, keyword); results.length > 0 && sessionStorage.setItem(cacheKey, JSON.stringify(results)); $container.data("_magnetData", results); $sortSelect.prop("disabled", results.length === 0); @@ -11651,7 +11653,7 @@ ${err.stack}` : ""); results = this.sortMagnetData(results, sortMode); const $table = $(` - + @@ -11662,9 +11664,9 @@ ${err.stack}` : ""); results.forEach(((result) => { const quality = classifyQuality(result.title); let badges = ""; - if (quality.is4K) badges += '4K'; - if (quality.isChinese) badges += '中字'; - if (quality.isCracked) badges += '破解'; + if (quality.is4K) badges += '4K'; + if (quality.isChinese) badges += '中字'; + if (quality.isCracked) badges += '破解'; const displayDate = formatMagnetDate(result.date); const dnCode = extractCode(result.title); const $row = $(` @@ -11679,7 +11681,7 @@ ${err.stack}` : ""); `); $tbody.append($row); @@ -11709,157 +11711,181 @@ ${err.stack}` : ""); } async resolveUrl(engine, keyword) { - let url = engine.url.replace("{keyword}", encodeURIComponent(keyword)); - if (engine.dynamicSearch2) { - let search2 = sessionStorage.getItem("jhs_u3c3_search2"); - if (!search2) { - search2 = await this.fetchSearch2(); - if (search2) sessionStorage.setItem("jhs_u3c3_search2", search2); - } - url = url.replace("{search2}", search2 || "c2pvbmy2"); - } - return url; - } - - async fetchSearch2() { - const result = await new Promise(((resolve) => { - _GM_xmlhttpRequest({ - method: "GET", - url: "https://u3c3.com/", - onload: (r) => resolve({html: r.responseText, headers: r.responseHeaders || ""}), - onerror: () => resolve(null) - }); - })); - if (!result) return null; - const cookie = parseSetCookie(result.headers); - if (cookie) sessionStorage.setItem("jhs_u3c3_cookie", cookie); - const match = result.html.match(/var\s+nmefafej\s*=\s*"([^"]+)"/g); - if (!match) return null; - for (const m of match) { - const val = m.match(/"([^"]+)"/)[1]; - if (val.length === 8) { - console.log("[U3C3] search2:", val); - return val; - } - } - return null; + return engine.url.replace("{keyword}", encodeURIComponent(keyword)); } async checkWhatslink(magnet) { var _a; + (_a = document.querySelector(".whatslink-overlay")) == null ? void 0 : _a.remove(); const overlay = document.createElement("div"); overlay.className = "whatslink-overlay"; - overlay.innerHTML = ''; - overlay.addEventListener("click", (e) => { - if (e.target === overlay) overlay.remove(); - }); + overlay.innerHTML = ''; document.body.appendChild(overlay); try { const url = `https://whatslink.info/api/v1/link?url=${encodeURIComponent(magnet)}`; - const r = await gmFetch(url); - if (!r.loadstuts) throw new Error("查询失败"); - const data = JSON.parse(r.responseText); - this.showWhatslinkModal(overlay, data, magnet); + const r = await gmFetch(url, {timeout: 2e4}); + if (!r.loadstuts) throw new Error("WhatsLink 请求失败"); + const data = JSON.parse(r.responseText || "{}"); + overlay.remove(); + this.showWhatslinkModal(data, magnet); } catch (e) { - overlay.innerHTML = ``; - (_a = overlay.querySelector(".whatslink-overlay")) == null ? void 0 : _a.classList.add("no-shots"); + overlay.remove(); + this.showWhatslinkModal({ + error: e.message || "查询失败", + name: "查询失败", + type: "-", + file_type: "-", + size: 0, + count: "-", + screenshots: [] + }, magnet); } } - showWhatslinkModal(overlay, data, magnet) { - const screenshots = Array.isArray(data.screenshots) ? data.screenshots : []; - const hasShots = screenshots.length > 0; - const sizeStr = data.size ? formatBytes(data.size) : "-"; - const fileCount = data.count || "-"; - overlay.innerHTML = ""; - const modal = document.createElement("div"); - modal.className = "whatslink-modal" + (hasShots ? "" : " no-shots"); - const scene = document.createElement("div"); - scene.className = "whatslink-gallery-scene"; - const visual = document.createElement("div"); - visual.className = "whatslink-gallery-visual"; - const closeBtn = document.createElement("button"); - closeBtn.className = "whatslink-gallery-close"; - closeBtn.innerHTML = "×"; - closeBtn.addEventListener("click", () => overlay.remove()); - visual.appendChild(closeBtn); - let currentIdx = 0; - const hero = document.createElement("img"); - hero.className = "whatslink-gallery-hero"; - if (hasShots) { - hero.src = screenshots[0]; - visual.appendChild(hero); - const prevBtn = document.createElement("button"); - prevBtn.className = "whatslink-gallery-arrow whatslink-gallery-prev"; - prevBtn.innerHTML = "‹"; - prevBtn.addEventListener("click", (e) => { - e.stopPropagation(); - currentIdx = (currentIdx - 1 + screenshots.length) % screenshots.length; - hero.src = screenshots[currentIdx]; - updateThumbs(); - updateInfo(); - }); - visual.appendChild(prevBtn); - const nextBtn = document.createElement("button"); - nextBtn.className = "whatslink-gallery-arrow whatslink-gallery-next"; - nextBtn.innerHTML = "›"; - nextBtn.addEventListener("click", (e) => { - e.stopPropagation(); - currentIdx = (currentIdx + 1) % screenshots.length; - hero.src = screenshots[currentIdx]; - updateThumbs(); - updateInfo(); - }); - visual.appendChild(nextBtn); - } else { - const emptyDiv = document.createElement("div"); - emptyDiv.className = "whatslink-gallery-empty"; - emptyDiv.innerHTML = ''; - visual.appendChild(emptyDiv); - } - const info = document.createElement("div"); - info.className = "whatslink-gallery-info"; - const updateInfo = () => { - info.innerHTML = `${currentIdx + 1} / ${screenshots.length}${sizeStr} · ${fileCount} 文件`; - }; - if (hasShots) updateInfo(); - else info.innerHTML = `${sizeStr} · ${fileCount} 文件`; - scene.appendChild(visual); - scene.appendChild(info); - if (hasShots && screenshots.length > 1) { - const thumbs = document.createElement("div"); - thumbs.className = "whatslink-gallery-thumbs"; - const updateThumbs2 = () => { - thumbs.innerHTML = ""; - screenshots.forEach((src, i) => { - const thumb = document.createElement("button"); - thumb.className = "whatslink-gallery-thumb" + (i === currentIdx ? " active" : ""); - const img = document.createElement("img"); - img.src = src; - thumb.appendChild(img); - thumb.addEventListener("click", (e) => { - e.stopPropagation(); - currentIdx = i; - hero.src = src; - updateThumbs2(); - updateInfo(); - }); - thumbs.appendChild(thumb); - }); - }; - updateThumbs2(); - scene.appendChild(thumbs); - } - modal.appendChild(scene); + showWhatslinkModal(payload, magnet) { + var _a; + (_a = document.querySelector(".whatslink-overlay")) == null ? void 0 : _a.remove(); + const shots = Array.isArray(payload == null ? void 0 : payload.screenshots) ? payload.screenshots.map((item) => item == null ? void 0 : item.screenshot).filter(Boolean) : []; + let index = 0; + const resourceName = (payload == null ? void 0 : payload.name) || "未知资源"; + const resourceCount = (payload == null ? void 0 : payload.count) ?? "-"; + const overlay = document.createElement("div"); + overlay.className = "whatslink-overlay"; + const modal = document.createElement("section"); + modal.className = `whatslink-modal${shots.length ? "" : " no-shots"}`; + modal.innerHTML = ``; overlay.appendChild(modal); + document.body.appendChild(overlay); + modal.querySelector(".whatslink-gallery-name").textContent = resourceName; + modal.querySelector(".whatslink-gallery-count").textContent = `${resourceCount} 个文件`; + const visual = modal.querySelector(".whatslink-gallery-visual"); + const hero = modal.querySelector(".whatslink-gallery-hero"); + const thumbs = modal.querySelector(".whatslink-gallery-thumbs"); + const currentIndex = modal.querySelector(".whatslink-gallery-index"); + const closeButton = modal.querySelector(".whatslink-gallery-close"); + const prevButton = modal.querySelector(".whatslink-gallery-prev"); + const nextButton = modal.querySelector(".whatslink-gallery-next"); + const sizeGallery = () => { + if (!shots.length || !hero.naturalWidth || !hero.naturalHeight) return; + const infoHeight = modal.querySelector(".whatslink-gallery-info").offsetHeight || 23; + const thumbsHeight = thumbs.offsetHeight || 58; + const maxHeight = Math.max(180, window.innerHeight - 36 - infoHeight - thumbsHeight - 6); + const maxWidth = modal.clientWidth; + const ratio = hero.naturalWidth / hero.naturalHeight; + const height = Math.min(maxHeight, maxWidth / ratio); + const width = height * ratio; + visual.style.width = `${width}px`; + visual.style.height = `${height}px`; + }; + const positionControls = () => { + if (!shots.length || !hero.naturalWidth || !hero.naturalHeight) return; + const width = visual.clientWidth; + const height = visual.clientHeight; + const scale = Math.min(width / hero.naturalWidth, height / hero.naturalHeight); + const imageWidth = hero.naturalWidth * scale; + const imageHeight = hero.naturalHeight * scale; + const imageLeft = (width - imageWidth) / 2; + const imageTop = (height - imageHeight) / 2; + const closeSize = closeButton.offsetWidth || 32; + const arrowSize = prevButton.offsetWidth || 34; + const sideGap = 12; + closeButton.style.left = `${imageLeft + imageWidth - closeSize - sideGap}px`; + closeButton.style.right = "auto"; + closeButton.style.top = `${imageTop + sideGap}px`; + prevButton.style.left = `${imageLeft + sideGap}px`; + prevButton.style.right = "auto"; + prevButton.style.top = `${imageTop + (imageHeight - arrowSize) / 2}px`; + nextButton.style.left = "auto"; + nextButton.style.right = `${width - imageLeft - imageWidth + sideGap}px`; + nextButton.style.top = `${imageTop + (imageHeight - arrowSize) / 2}px`; + }; + const render = () => { + modal.classList.toggle("has-shots", Boolean(shots.length)); + if (!shots.length) { + currentIndex.textContent = "NO PREVIEW"; + return; + } + hero.src = shots[index]; + const frame = `${index + 1} / ${shots.length}`; + currentIndex.textContent = frame; + [...thumbs.children].forEach((btn, i) => btn.classList.toggle("active", i === index)); + if (hero.complete) { + sizeGallery(); + positionControls(); + } + }; + shots.forEach((url, i) => { + const btn = document.createElement("button"); + btn.type = "button"; + btn.className = "whatslink-gallery-thumb"; + btn.innerHTML = `截图${i + 1}`; + btn.addEventListener("click", () => { + index = i; + render(); + }); + thumbs.appendChild(btn); + }); + prevButton.addEventListener("click", () => { + if (!shots.length) return; + index = (index + shots.length - 1) % shots.length; + render(); + }); + nextButton.addEventListener("click", () => { + if (!shots.length) return; + index = (index + 1) % shots.length; + render(); + }); + const onKeydown = (event) => { + if (!document.body.contains(overlay)) return; + if (event.key === "ArrowLeft" && shots.length) { + index = (index + shots.length - 1) % shots.length; + render(); + } + if (event.key === "ArrowRight" && shots.length) { + index = (index + 1) % shots.length; + render(); + } + if (event.key === "Escape") close(); + }; + const onResize = () => { + sizeGallery(); + positionControls(); + }; + const removeResize = () => window.removeEventListener("resize", onResize); + const close = () => { + overlay.remove(); + document.removeEventListener("keydown", onKeydown); + removeResize(); + }; + closeButton.addEventListener("click", close); + overlay.addEventListener("click", (e) => { + if (e.target === overlay) close(); + }); + hero.addEventListener("load", () => { + sizeGallery(); + positionControls(); + }); + window.addEventListener("resize", onResize); + document.addEventListener("keydown", onKeydown); + render(); } parseU9A9(html, keyword) { @@ -11883,25 +11909,42 @@ ${err.stack}` : ""); return results; } - parseU3C3(html, keyword) { - const $dom = utils.htmlTo$dom(html), results = []; - const kwNorm = String(keyword).toUpperCase().replace(/[^A-Z0-9]/g, ""); - $dom.find(".torrent-list tbody tr").each(((i, el) => { - const $el = $(el); - if ($el.text().includes("置顶")) return; - const $titleA = $el.find("td:nth-child(2) a"); - const title = $titleA.attr("title") || $titleA.text().trim(); - if (!title) return; - if (kwNorm) { - const titleNorm = title.toUpperCase().replace(/[^A-Z0-9]/g, ""); - if (!titleNorm.includes(kwNorm)) return; - } - const magnet = $el.find("td:nth-child(3) a[href^='magnet:']").attr("href"); - const size = $el.find("td:nth-child(4)").text().trim(); - const date = $el.find("td:nth-child(5)").text().trim(); - magnet && results.push({title, magnet, size, date}); - })); - return results; + async parseU3C3(_html, keyword) { + var _a; + const base = "https://www.u3c3.com"; + const referer = base + "/"; + const home = await gmFetch(base + "/", { + headers: {Referer: referer} + }); + if (!home.loadstuts) return []; + const homeDoc = parseHTML(home.responseText); + const searchScript = [...homeDoc.scripts].map((script) => script.textContent || "").find((text) => text.includes("function search21")) || ""; + const token = ((_a = searchScript.match(/^\s*var\s+nmefafej\s*=\s*["']([^"']+)["'];?/m)) == null ? void 0 : _a[1]) || ""; + if (!token) return []; + const searchUrl = `${base}/?search2=${encodeURIComponent(token)}&search=${encodeURIComponent(keyword)}`; + const r = await gmFetch(searchUrl, { + headers: {Referer: referer} + }); + if (!r.loadstuts) return []; + const doc = parseHTML(r.responseText); + const normalizedKeyword = String(keyword || "").toUpperCase().replace(/[-_\s]/g, ""); + const data = [...doc.querySelectorAll("table.torrent-list tbody tr.default, table.torrent-list tbody tr.success")].map((row) => { + var _a2, _b, _c, _d, _e, _f; + const titleA = row.querySelector('td:nth-child(2) a[href*="/view?id="]'); + const magnetA = row.querySelector('td:nth-child(3) a[href^="magnet:"]'); + const title = ((_a2 = titleA == null ? void 0 : titleA.getAttribute("title")) == null ? void 0 : _a2.trim()) || ((_b = titleA == null ? void 0 : titleA.textContent) == null ? void 0 : _b.trim()) || ""; + if (!title || !(magnetA == null ? void 0 : magnetA.href)) return null; + if (normalizedKeyword && !title.toUpperCase().replace(/[-_\s]/g, "").includes(normalizedKeyword)) return null; + const href = titleA.getAttribute("href") || ""; + return { + title, + magnet: magnetA.href, + size: ((_d = (_c = row.querySelector("td:nth-child(4)")) == null ? void 0 : _c.textContent) == null ? void 0 : _d.trim()) || "", + date: ((_f = (_e = row.querySelector("td:nth-child(5)")) == null ? void 0 : _e.textContent) == null ? void 0 : _f.trim()) || "", + src: href ? new URL(href, base).href : searchUrl + }; + }).filter(Boolean); + return data; } parseSukebei(html, keyword) { @@ -12030,20 +12073,6 @@ ${err.stack}` : ""); } } - function parseSetCookie(headers) { - if (!headers) return ""; - const lines = headers.split("\n"); - const cookies = []; - for (const line of lines) { - const match = line.match(/^set-cookie:\s*(.+?)(?:;|$)/i); - if (match) { - const val = match[1].split(";")[0].trim(); - if (val) cookies.push(val); - } - } - return cookies.join("; "); - } - function formatBytes(bytes) { if (!bytes || bytes <= 0) return ""; const units = ["B", "KB", "MB", "GB", "TB"]; @@ -12097,7 +12126,7 @@ ${err.stack}` : ""); $container.empty(); const $table = $(`
名称名称 大小 日期 操作
- +
- + @@ -12108,9 +12137,9 @@ ${err.stack}` : ""); results.forEach(((result) => { const quality = classifyQuality(result.title); let badges = ""; - if (quality.is4K) badges += '4K'; - if (quality.isChinese) badges += '中字'; - if (quality.isCracked) badges += '破解'; + if (quality.is4K) badges += '4K'; + if (quality.isChinese) badges += '中字'; + if (quality.isCracked) badges += '破解'; const displayDate = formatMagnetDate(result.date); const dnCode = extractCode(result.title); const $row = $(` diff --git a/src/plugins/magnet-hub.js b/src/plugins/magnet-hub.js index eba7484..1632191 100644 --- a/src/plugins/magnet-hub.js +++ b/src/plugins/magnet-hub.js @@ -119,10 +119,9 @@ class MagnetHubPlugin extends BasePlugin { }, { name: "U3C3", id: "u3c3", - url: "https://u3c3.com/?search2={search2}&search={keyword}", - targetPage: "https://u3c3.com/?search2={search2}&search={keyword}", - parseHtml: this.parseU3C3, - dynamicSearch2: true + url: "https://u3c3.com/?search={keyword}", + targetPage: "https://u3c3.com/?search={keyword}", + parseHtml: this.parseU3C3 }, { name: "BTSearch", id: "btsearch", @@ -256,7 +255,7 @@ class MagnetHubPlugin extends BasePlugin { color: #243b67; font-weight: 650; text-decoration: none; - line-height: 1; + line-height: 1.5; } .magnet-name-cell > a:hover { color: #1d4ed8; @@ -270,7 +269,7 @@ class MagnetHubPlugin extends BasePlugin { border-radius: 3px; font-size: 11px; font-weight: 800; - line-height: 1; + line-height: 1.5; color: #fff; } .magnet-badge.mg-subtitle { background: #16a34a; } @@ -345,7 +344,7 @@ class MagnetHubPlugin extends BasePlugin { .whatslink-overlay { position: fixed; inset: 0; - z-index: 10000040; + z-index: 2147483700; display: flex; align-items: center; justify-content: center; @@ -592,10 +591,6 @@ class MagnetHubPlugin extends BasePlugin { sessionStorage.getItem(cacheKey); this.resolveUrl(engine, keyword).then((url => { const requestOpts = {method: "GET", url: url}; - if (engine.dynamicSearch2) { - const cookie = sessionStorage.getItem("jhs_u3c3_cookie"); - if (cookie) requestOpts.cookie = cookie; - } if (engine.parseHtml) { GM_xmlhttpRequest(Object.assign(requestOpts, { method: "GET", @@ -687,156 +682,179 @@ class MagnetHubPlugin extends BasePlugin { } async resolveUrl(engine, keyword) { - let url = engine.url.replace("{keyword}", encodeURIComponent(keyword)); - if (engine.dynamicSearch2) { - let search2 = sessionStorage.getItem("jhs_u3c3_search2"); - if (!search2) { - search2 = await this.fetchSearch2(); - if (search2) sessionStorage.setItem("jhs_u3c3_search2", search2); - } - url = url.replace("{search2}", search2 || "c2pvbmy2"); - } - return url; - } - - async fetchSearch2() { - const result = await new Promise((resolve => { - GM_xmlhttpRequest({ - method: "GET", - url: "https://u3c3.com/", - onload: r => resolve({html: r.responseText, headers: r.responseHeaders || ""}), - onerror: () => resolve(null) - }); - })); - if (!result) return null; - const cookie = parseSetCookie(result.headers); - if (cookie) sessionStorage.setItem("jhs_u3c3_cookie", cookie); - const match = result.html.match(/var\s+nmefafej\s*=\s*"([^"]+)"/g); - if (!match) return null; - for (const m of match) { - const val = m.match(/"([^"]+)"/)[1]; - if (val.length === 8) { - console.log("[U3C3] search2:", val); - return val; - } - } - return null; + return engine.url.replace("{keyword}", encodeURIComponent(keyword)); } async checkWhatslink(magnet) { + document.querySelector('.whatslink-overlay')?.remove(); const overlay = document.createElement('div'); overlay.className = 'whatslink-overlay'; - overlay.innerHTML = ''; - overlay.addEventListener('click', e => { - if (e.target === overlay) overlay.remove(); - }); + overlay.innerHTML = ''; document.body.appendChild(overlay); try { const url = `https://whatslink.info/api/v1/link?url=${encodeURIComponent(magnet)}`; - const r = await gmFetch(url); - if (!r.loadstuts) throw new Error('查询失败'); - const data = JSON.parse(r.responseText); - this.showWhatslinkModal(overlay, data, magnet); + const r = await gmFetch(url, {timeout: 20000}); + if (!r.loadstuts) throw new Error('WhatsLink 请求失败'); + const data = JSON.parse(r.responseText || '{}'); + overlay.remove(); + this.showWhatslinkModal(data, magnet); } catch (e) { - overlay.innerHTML = ``; - overlay.querySelector('.whatslink-overlay')?.classList.add('no-shots'); + overlay.remove(); + this.showWhatslinkModal({ + error: e.message || '查询失败', + name: '查询失败', + type: '-', + file_type: '-', + size: 0, + count: '-', + screenshots: [] + }, magnet); } } - showWhatslinkModal(overlay, data, magnet) { - const screenshots = Array.isArray(data.screenshots) ? data.screenshots : []; - const hasShots = screenshots.length > 0; - const sizeStr = data.size ? formatBytes(data.size) : '-'; - const fileCount = data.count || '-'; - overlay.innerHTML = ''; - const modal = document.createElement('div'); - modal.className = 'whatslink-modal' + (hasShots ? '' : ' no-shots'); - const scene = document.createElement('div'); - scene.className = 'whatslink-gallery-scene'; - const visual = document.createElement('div'); - visual.className = 'whatslink-gallery-visual'; - const closeBtn = document.createElement('button'); - closeBtn.className = 'whatslink-gallery-close'; - closeBtn.innerHTML = '×'; - closeBtn.addEventListener('click', () => overlay.remove()); - visual.appendChild(closeBtn); - let currentIdx = 0; - const hero = document.createElement('img'); - hero.className = 'whatslink-gallery-hero'; - if (hasShots) { - hero.src = screenshots[0]; - visual.appendChild(hero); - const prevBtn = document.createElement('button'); - prevBtn.className = 'whatslink-gallery-arrow whatslink-gallery-prev'; - prevBtn.innerHTML = '‹'; - prevBtn.addEventListener('click', e => { - e.stopPropagation(); - currentIdx = (currentIdx - 1 + screenshots.length) % screenshots.length; - hero.src = screenshots[currentIdx]; - updateThumbs(); - updateInfo(); - }); - visual.appendChild(prevBtn); - const nextBtn = document.createElement('button'); - nextBtn.className = 'whatslink-gallery-arrow whatslink-gallery-next'; - nextBtn.innerHTML = '›'; - nextBtn.addEventListener('click', e => { - e.stopPropagation(); - currentIdx = (currentIdx + 1) % screenshots.length; - hero.src = screenshots[currentIdx]; - updateThumbs(); - updateInfo(); - }); - visual.appendChild(nextBtn); - } else { - const emptyDiv = document.createElement('div'); - emptyDiv.className = 'whatslink-gallery-empty'; - emptyDiv.innerHTML = ''; - visual.appendChild(emptyDiv); - } - const info = document.createElement('div'); - info.className = 'whatslink-gallery-info'; - const updateInfo = () => { - info.innerHTML = `${currentIdx + 1} / ${screenshots.length}${sizeStr} · ${fileCount} 文件`; - }; - if (hasShots) updateInfo(); - else info.innerHTML = `${sizeStr} · ${fileCount} 文件`; - scene.appendChild(visual); - scene.appendChild(info); - if (hasShots && screenshots.length > 1) { - const thumbs = document.createElement('div'); - thumbs.className = 'whatslink-gallery-thumbs'; - const updateThumbs = () => { - thumbs.innerHTML = ''; - screenshots.forEach((src, i) => { - const thumb = document.createElement('button'); - thumb.className = 'whatslink-gallery-thumb' + (i === currentIdx ? ' active' : ''); - const img = document.createElement('img'); - img.src = src; - thumb.appendChild(img); - thumb.addEventListener('click', e => { - e.stopPropagation(); - currentIdx = i; - hero.src = src; - updateThumbs(); - updateInfo(); - }); - thumbs.appendChild(thumb); - }); - }; - updateThumbs(); - scene.appendChild(thumbs); - } - modal.appendChild(scene); + showWhatslinkModal(payload, magnet) { + document.querySelector('.whatslink-overlay')?.remove(); + const shots = Array.isArray(payload?.screenshots) ? payload.screenshots.map(item => item?.screenshot).filter(Boolean) : []; + let index = 0; + const resourceName = payload?.name || '未知资源'; + const resourceCount = payload?.count ?? '-'; + const overlay = document.createElement('div'); + overlay.className = 'whatslink-overlay'; + const modal = document.createElement('section'); + modal.className = `whatslink-modal${shots.length ? '' : ' no-shots'}`; + modal.innerHTML = ``; overlay.appendChild(modal); + document.body.appendChild(overlay); + modal.querySelector('.whatslink-gallery-name').textContent = resourceName; + modal.querySelector('.whatslink-gallery-count').textContent = `${resourceCount} 个文件`; + const visual = modal.querySelector('.whatslink-gallery-visual'); + const hero = modal.querySelector('.whatslink-gallery-hero'); + const thumbs = modal.querySelector('.whatslink-gallery-thumbs'); + const currentIndex = modal.querySelector('.whatslink-gallery-index'); + const closeButton = modal.querySelector('.whatslink-gallery-close'); + const prevButton = modal.querySelector('.whatslink-gallery-prev'); + const nextButton = modal.querySelector('.whatslink-gallery-next'); + const sizeGallery = () => { + if (!shots.length || !hero.naturalWidth || !hero.naturalHeight) return; + const infoHeight = modal.querySelector('.whatslink-gallery-info').offsetHeight || 23; + const thumbsHeight = thumbs.offsetHeight || 58; + const maxHeight = Math.max(180, window.innerHeight - 36 - infoHeight - thumbsHeight - 6); + const maxWidth = modal.clientWidth; + const ratio = hero.naturalWidth / hero.naturalHeight; + const height = Math.min(maxHeight, maxWidth / ratio); + const width = height * ratio; + visual.style.width = `${width}px`; + visual.style.height = `${height}px`; + }; + const positionControls = () => { + if (!shots.length || !hero.naturalWidth || !hero.naturalHeight) return; + const width = visual.clientWidth; + const height = visual.clientHeight; + const scale = Math.min(width / hero.naturalWidth, height / hero.naturalHeight); + const imageWidth = hero.naturalWidth * scale; + const imageHeight = hero.naturalHeight * scale; + const imageLeft = (width - imageWidth) / 2; + const imageTop = (height - imageHeight) / 2; + const closeSize = closeButton.offsetWidth || 32; + const arrowSize = prevButton.offsetWidth || 34; + const sideGap = 12; + closeButton.style.left = `${imageLeft + imageWidth - closeSize - sideGap}px`; + closeButton.style.right = 'auto'; + closeButton.style.top = `${imageTop + sideGap}px`; + prevButton.style.left = `${imageLeft + sideGap}px`; + prevButton.style.right = 'auto'; + prevButton.style.top = `${imageTop + (imageHeight - arrowSize) / 2}px`; + nextButton.style.left = 'auto'; + nextButton.style.right = `${width - imageLeft - imageWidth + sideGap}px`; + nextButton.style.top = `${imageTop + (imageHeight - arrowSize) / 2}px`; + }; + const render = () => { + modal.classList.toggle('has-shots', Boolean(shots.length)); + if (!shots.length) { + currentIndex.textContent = 'NO PREVIEW'; + return; + } + hero.src = shots[index]; + const frame = `${index + 1} / ${shots.length}`; + currentIndex.textContent = frame; + [...thumbs.children].forEach((btn, i) => btn.classList.toggle('active', i === index)); + if (hero.complete) { + sizeGallery(); + positionControls(); + } + }; + shots.forEach((url, i) => { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'whatslink-gallery-thumb'; + btn.innerHTML = `截图${i + 1}`; + btn.addEventListener('click', () => { + index = i; + render(); + }); + thumbs.appendChild(btn); + }); + prevButton.addEventListener('click', () => { + if (!shots.length) return; + index = (index + shots.length - 1) % shots.length; + render(); + }); + nextButton.addEventListener('click', () => { + if (!shots.length) return; + index = (index + 1) % shots.length; + render(); + }); + const onKeydown = event => { + if (!document.body.contains(overlay)) return; + if (event.key === 'ArrowLeft' && shots.length) { + index = (index + shots.length - 1) % shots.length; + render(); + } + if (event.key === 'ArrowRight' && shots.length) { + index = (index + 1) % shots.length; + render(); + } + if (event.key === 'Escape') close(); + }; + const onResize = () => { + sizeGallery(); + positionControls(); + }; + const removeResize = () => window.removeEventListener('resize', onResize); + const close = () => { + overlay.remove(); + document.removeEventListener('keydown', onKeydown); + removeResize(); + }; + closeButton.addEventListener('click', close); + overlay.addEventListener('click', e => { + if (e.target === overlay) close(); + }); + hero.addEventListener('load', () => { + sizeGallery(); + positionControls(); + }); + window.addEventListener('resize', onResize); + document.addEventListener('keydown', onKeydown); + render(); } parseU9A9(html, keyword) { @@ -860,25 +878,40 @@ class MagnetHubPlugin extends BasePlugin { return results; } - parseU3C3(html, keyword) { - const $dom = utils.htmlTo$dom(html), results = []; - const kwNorm = String(keyword).toUpperCase().replace(/[^A-Z0-9]/g, ''); - $dom.find(".torrent-list tbody tr").each(((i, el) => { - const $el = $(el); - if ($el.text().includes("置顶")) return; - const $titleA = $el.find("td:nth-child(2) a"); - const title = $titleA.attr("title") || $titleA.text().trim(); - if (!title) return; - if (kwNorm) { - const titleNorm = title.toUpperCase().replace(/[^A-Z0-9]/g, ''); - if (!titleNorm.includes(kwNorm)) return; - } - const magnet = $el.find("td:nth-child(3) a[href^='magnet:']").attr("href"); - const size = $el.find("td:nth-child(4)").text().trim(); - const date = $el.find("td:nth-child(5)").text().trim(); - magnet && results.push({title, magnet, size, date}); - })); - return results; + async parseU3C3(_html, keyword) { + const base = 'https://www.u3c3.com'; + const referer = base + '/'; + const home = await gmFetch(base + '/', { + headers: {Referer: referer}, + }); + if (!home.loadstuts) return []; + const homeDoc = parseHTML(home.responseText); + const searchScript = [...homeDoc.scripts].map(script => script.textContent || '').find(text => text.includes('function search21')) || ''; + const token = searchScript.match(/^\s*var\s+nmefafej\s*=\s*["']([^"']+)["'];?/m)?.[1] || ''; + if (!token) return []; + const searchUrl = `${base}/?search2=${encodeURIComponent(token)}&search=${encodeURIComponent(keyword)}`; + const r = await gmFetch(searchUrl, { + headers: {Referer: referer}, + }); + if (!r.loadstuts) return []; + const doc = parseHTML(r.responseText); + const normalizedKeyword = String(keyword || '').toUpperCase().replace(/[-_\s]/g, ''); + const data = [...doc.querySelectorAll('table.torrent-list tbody tr.default, table.torrent-list tbody tr.success')].map(row => { + const titleA = row.querySelector('td:nth-child(2) a[href*="/view?id="]'); + const magnetA = row.querySelector('td:nth-child(3) a[href^="magnet:"]'); + const title = titleA?.getAttribute('title')?.trim() || titleA?.textContent?.trim() || ''; + if (!title || !magnetA?.href) return null; + if (normalizedKeyword && !title.toUpperCase().replace(/[-_\s]/g, '').includes(normalizedKeyword)) return null; + const href = titleA.getAttribute('href') || ''; + return { + title, + magnet: magnetA.href, + size: row.querySelector('td:nth-child(4)')?.textContent?.trim() || '', + date: row.querySelector('td:nth-child(5)')?.textContent?.trim() || '', + src: href ? new URL(href, base).href : searchUrl, + }; + }).filter(Boolean); + return data; } parseSukebei(html, keyword) { @@ -1006,20 +1039,6 @@ class MagnetHubPlugin extends BasePlugin { } } -function parseSetCookie(headers) { - if (!headers) return ""; - const lines = headers.split("\n"); - const cookies = []; - for (const line of lines) { - const match = line.match(/^set-cookie:\s*(.+?)(?:;|$)/i); - if (match) { - const val = match[1].split(";")[0].trim(); - if (val) cookies.push(val); - } - } - return cookies.join("; "); -} - function formatBytes(bytes) { if (!bytes || bytes <= 0) return ''; const units = ['B', 'KB', 'MB', 'GB', 'TB'];
名称名称 大小 日期 操作