import {BasePlugin} from '../core/base-plugin.js';
import {__publicField} from '../core/constants.js';
import {GM_xmlhttpRequest} from 'vite-plugin-monkey/dist/client';
function showCopiedFeedback($btn) {
const originalText = $btn.text();
$btn.addClass("copied").text("已复制");
setTimeout((() => {
$btn.removeClass("copied").text(originalText);
}), 2e3);
}
function fallbackCopy(text, $btn) {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand("copy");
showCopiedFeedback($btn);
} catch (err) {
console.error("复制失败:", err);
alert("复制失败,请手动复制链接");
}
document.body.removeChild(textarea);
}
function parseSize(sizeStr) {
if (!sizeStr || sizeStr === "未知") return 0;
const match = String(sizeStr).replace(/,/g, '').match(/([\d.]+)\s*(TiB|GiB|MiB|KiB|TB|GB|MB|KB|B)?/i);
if (!match) return 0;
const num = parseFloat(match[1]);
const unit = (match[2] || 'B').toUpperCase();
const multipliers = {
TIB: 1099511627776,
TB: 1099511627776,
GIB: 1073741824,
GB: 1073741824,
MIB: 1048576,
MB: 1048576,
KIB: 1024,
KB: 1024,
B: 1
};
return Number.isFinite(num) ? num * (multipliers[unit] || 1) : 0;
}
function parseDate(dateStr) {
if (!dateStr || dateStr === "未知") return 0;
const ts = Date.parse(dateStr.replace(/\//g, '-'));
return isNaN(ts) ? 0 : ts;
}
function formatMagnetDate(dateStr) {
if (!dateStr) return '';
const match = String(dateStr).match(/(\d{4})[-/](\d{1,2})[-/](\d{1,2})/);
if (match) return `${match[1]}-${match[2].padStart(2, '0')}-${match[3].padStart(2, '0')}`;
const ts = parseDate(dateStr);
return ts ? new Date(ts).toISOString().slice(0, 10) : dateStr;
}
function classifyQuality(title) {
const text = String(title || '').replace(/【[^】]*(?:APP|夸克|UC搜|(?:[a-z0-9-]+\.)+(?:com|net|org|cc|sbs|top))[^】]*】/gi, ' ');
const hasCJK = /[\u4e00-\u9fff]/.test(text);
const hasJP = /[\u3040-\u309f\u30a0-\u30ff]/.test(text);
const isChinese = /(?:[^A-Za-z]|^)FHDC(?:[^A-Za-z]|$)/i.test(text)
|| /[-_](?:UC|CH?)(?:[^A-Za-z]|$)/.test(text)
|| /(?:\u4e2d\u5b57|\u4e2d\u6587|\u5b57\u5e55|\u4e2d\u6587\u5b57\u5e55|\u7e41\u9ad4\u4e2d\u5b57|\u7e41\u4f53\u4e2d\u5b57|\u7e41\u9ad4\u4e2d\u6587|\u7e41\u4f53\u4e2d\u6587|\u7e41\u9ad4\u5b57\u5e55|\u7e41\u4f53\u5b57\u5e55|\u7e41\u4e2d|\u7e41\u5b57|\u81ea\u63d0|\u5f81\u7528|\u5fb5\u7528|\u6f22\u5316|\u6c49\u5316|\u5167\u5d4c|\u5185\u5d4c|\u5167\u5c01|\u5185\u5c01|\u96d9\u8a9e|\u53cc\u8bed)/.test(text)
|| (hasCJK && !hasJP);
const is4K = /(?:[^A-Za-z0-9]|^)(?:4K(?:UHD)?|2160P)(?:[^A-Za-z0-9]|$)/i.test(text);
const isCracked = /(?:\u7834\u89e3|\u7834\u574f|\u7834\u58de|\u7834\u58ca|\u65e0\u7801|\u7121\u78bc)/.test(text)
|| /\b(?:uncensored|mosaic)\b/i.test(text);
return {isChinese, is4K, isCracked};
}
function gmFetch(url, opts = {}) {
return new Promise((resolve => {
GM_xmlhttpRequest({
method: opts.method || "GET",
url: url,
headers: opts.headers || {},
data: opts.data,
onload: r => {
resolve({
loadstuts: r.status >= 200 && r.status < 300,
status: r.status,
responseText: r.responseText,
finalUrl: r.finalUrl || url
});
},
onerror: () => resolve({loadstuts: false, status: 0, responseText: '', finalUrl: url})
});
}));
}
function parseHTML(html) {
const parser = new DOMParser();
return parser.parseFromString(html, "text/html");
}
class MagnetHubPlugin extends BasePlugin {
constructor() {
super(...arguments);
__publicField(this, "currentEngine", null);
__publicField(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/?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";
}
async initCss() {
return `
`;
}
createMagnetHub(keyword) {
keyword = keyword.replace("FC2-", "");
const $container = $('
');
const $tabs = $('');
const key = "jhs_magnetHub_selectedEngine";
const savedEngineId = localStorage.getItem(key);
let defaultEngineIndex = 0;
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 (!savedEngineId && engine.id === "Sukebei") {
$tab.addClass("active");
this.currentEngine = engine;
}
$tabBox.append($tab);
}));
const sortMode = localStorage.getItem("jhs_magnetSort") || "size";
const $sortSelect = $(``);
$tabBox.append($sortSelect);
$tabs.append($tabBox);
$tabs.append(`原网页`);
$container.append($tabs);
const $resultsContainer = $('');
$container.append($resultsContainer);
$container.on("click", ".magnet-tab", (e => {
const engineId = $(e.target).data("engine");
this.currentEngine = this.searchEngines.find((engine => engine.id === engineId));
this.resolveUrl(this.currentEngine, keyword).then((url => {
$("#targetBox").attr("href", url);
}));
localStorage.setItem(key, engineId);
$container.find(".magnet-tab").removeClass("active");
$(e.target).addClass("active");
this.searchEngine($resultsContainer, $sortSelect, this.currentEngine, keyword);
}));
$sortSelect.on("change", () => {
localStorage.setItem("jhs_magnetSort", $sortSelect.val());
RerenderSort($resultsContainer, $sortSelect.val());
});
$container.on("click", ".copy-btn", function () {
const $btn = $(this), magnet = $btn.data("magnet");
const dnCode = $btn.data("dncode") || '';
const magWithDn = magnet + (dnCode ? `&dn=${encodeURIComponent(dnCode)}` : '');
navigator.clipboard ? navigator.clipboard.writeText(magWithDn).then((() => {
showCopiedFeedback($btn);
})).catch((() => {
fallbackCopy(magWithDn, $btn);
})) : fallbackCopy(magWithDn, $btn);
});
$container.on("click", ".down-115", (async event => {
event.stopPropagation();
const magnet = $(event.currentTarget).data("magnet");
let loadObj = loading();
try {
await this.getBean("WangPan115TaskPlugin").handleAddTask(magnet);
} catch (e) {
show.error("发生错误:" + e);
console.error(e);
} finally {
loadObj.close();
}
}));
$container.on("click", ".check-btn", (async event => {
event.stopPropagation();
const magnet = $(event.currentTarget).data("magnet");
this.checkWhatslink(magnet);
}));
this.searchEngine($resultsContainer, $sortSelect, this.currentEngine || this.searchEngines[defaultEngineIndex], keyword);
this.resolveUrl(this.currentEngine || this.searchEngines[defaultEngineIndex], keyword).then((url => {
$("#targetBox").attr("href", url);
}));
return $container;
}
searchEngine($container, $sortSelect, engine, keyword) {
this.currentEngine = engine;
this.currentKeyword = keyword;
$container.html(`正在从 ${engine.name} 搜索 "${keyword}"...
`);
const cacheKey = `${engine.name}_${keyword}`;
sessionStorage.getItem(cacheKey);
this.resolveUrl(engine, keyword).then((url => {
const requestOpts = {method: "GET", url: url};
if (engine.parseHtml) {
GM_xmlhttpRequest(Object.assign(requestOpts, {
method: "GET",
url: url,
onload: async response => {
try {
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);
this.displayResults($container, results, engine.name);
} catch (e) {
this.showError($container, `解析 ${engine.name} 结果失败: ${e.message}`);
}
},
onerror: error => {
this.showError($container, `从 ${engine.name} 获取数据失败: ${error.statusText}`);
}
}));
}
if (engine.parseJson) {
engine.parseJson.call(this, $container, $sortSelect, engine, keyword, cacheKey);
}
}));
}
showError($container, message) {
$container.html(``);
const $select = $container.closest(".magnet-container").find(".magnet-sort-select");
$container.find("#retrySearch").on("click", (e => {
e.preventDefault();
this.searchEngine($container, $select, this.currentEngine, this.currentKeyword);
}));
}
displayResults($container, results, engineName) {
$container.empty();
const sortMode = localStorage.getItem("jhs_magnetSort") || "size";
if (0 !== results.length) {
results = this.sortMagnetData(results, sortMode);
const $table = $(``);
const $tbody = $table.find("tbody");
results.forEach((result => {
const quality = classifyQuality(result.title);
let 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 = $(`
|
|
${result.size || '未知'} |
${displayDate || '未知'} |
|
`);
$tbody.append($row);
}));
$container.append($table);
} else {
$container.append(``);
$("#retrySearch").on("click", (e => {
e.preventDefault();
this.searchEngine($container, $container.closest(".magnet-container").find(".magnet-sort-select"), this.currentEngine, this.currentKeyword);
}));
}
}
sortMagnetData(data, mode) {
return [...data].sort((a, b) => {
const sizeDelta = parseSize(b.size) - parseSize(a.size);
if (mode === 'size') return sizeDelta;
const aTime = parseDate(a.date);
const bTime = parseDate(b.date);
if (!aTime && !bTime) return sizeDelta;
if (!aTime) return 1;
if (!bTime) return -1;
const timeDelta = mode === 'oldest' ? aTime - bTime : bTime - aTime;
return timeDelta || sizeDelta;
});
}
async resolveUrl(engine, keyword) {
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 = '';
document.body.appendChild(overlay);
try {
const url = `https://whatslink.info/api/v1/link?url=${encodeURIComponent(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.remove();
this.showWhatslinkModal({
error: e.message || '查询失败',
name: '查询失败',
type: '-',
file_type: '-',
size: 0,
count: '-',
screenshots: []
}, magnet);
}
}
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 = `
`;
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) {
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").first();
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) {
const $dom = utils.htmlTo$dom(html), results = [];
$dom.find("tr.default, tr.success").each(((i, el) => {
const $el = $(el);
const title = $el.find("td:nth-child(2) a").first().attr("title") || $el.find("td:nth-child(2) a").first().text().trim();
if (!title) return;
const magnet = $el.find("td:nth-child(3) a:last-child").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 parseOmag(html, keyword) {
const doc = parseHTML(html);
const base = "https://xn--mag-zna.net";
const entries = [...doc.querySelectorAll('tr')].map(row => {
const titleA = row.querySelector('td.result-title a[href^="/!"]');
if (!titleA) return null;
const href = titleA.getAttribute('href') || '';
if (!href) return null;
let src;
try {
src = new URL(href, base).href;
} catch (_) {
return null;
}
const title = titleA.textContent.trim();
const meta = [...row.querySelectorAll('.result-meta > div')].map(item => item.textContent.trim());
return {title, size: meta[0] || '', date: meta[1] || '', src};
}).filter(Boolean);
const searchUrl = `https://xn--mag-zna.net/search?q=${encodeURIComponent(keyword)}`;
const results = [];
for (const entry of entries) {
try {
const r = await gmFetch(entry.src, {
headers: {Referer: searchUrl}
});
if (!r.loadstuts) continue;
const detailDoc = parseHTML(r.responseText);
const magnetInput = detailDoc.querySelector('#input-magnet');
const magnetA = detailDoc.querySelector('a[href^="magnet:"]');
const maglink = (magnetInput?.getAttribute('value') || magnetA?.getAttribute('href') || magnetA?.href || '').trim();
if (!maglink || !/^magnet:\?xt=urn:btih:/i.test(maglink)) continue;
results.push({title: entry.title, magnet: maglink, size: entry.size, date: entry.date});
} catch (_) {
}
}
return results;
}
parseBTSearch($container, $sortSelect, engine, keyword, cacheKey) {
const _this = this;
const apiUrl = engine.url.replace("{keyword}", encodeURIComponent(keyword));
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = randomString(8);
const parts = [`timestamp=${timestamp}`, `nonce=${nonce}`, `keyword=${keyword}`, `limit=10`, `offset=0`, `sort=size`, `sort_type=desc`];
const signText = `${parts.sort().join('&')}&key=long2ice`;
const sign = window.md5 ? window.md5(signText).toUpperCase() : '';
GM_xmlhttpRequest({
method: "GET",
url: apiUrl,
headers: {
Accept: 'application/json',
Referer: engine.targetPage.replace("{keyword}", encodeURIComponent(keyword)),
'x-timestamp': timestamp,
'x-nonce': nonce,
'x-sign': sign
},
onload: response => {
try {
const json = JSON.parse(response.responseText);
const items = Array.isArray(json?.data) ? json.data : Array.isArray(json?.data?.data) ? json.data.data : [];
const results = items.map(item => {
const title = String(item?.name || '').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
const hash = String(item?.hash || '').replace(/^magnet:\?xt=urn:btih:/i, '').replace(/[^a-z0-9]/gi, '');
if (!/^[a-f0-9]{32,40}$/i.test(hash)) return null;
const size = formatBytes(item?.size);
const date = item?.created_at || item?.created_time || item?.create_time || '';
return {
title: title || `magnet:?xt=urn:btih:${hash}`,
magnet: `magnet:?xt=urn:btih:${hash}`,
size,
date
};
}).filter(Boolean);
results.length > 0 && sessionStorage.setItem(cacheKey, JSON.stringify(results));
$container.data("_magnetData", results);
$sortSelect.prop("disabled", results.length === 0);
_this.displayResults($container, results, engine.name);
} catch (e) {
_this.showError($container, `解析 ${engine.name} 结果失败: ${e.message}`);
}
},
onerror: error => {
_this.showError($container, `从 ${engine.name} 获取数据失败: ${error.statusText}`);
}
});
}
parseSokitty(html, keyword) {
const doc = parseHTML(html);
const base = "https://w1.sokitty.me";
const kwNorm = String(keyword).toUpperCase().replace(/[-_\s]/g, '');
const results = [];
doc.querySelectorAll('.panel.search-panel').forEach(panel => {
const titleA = panel.querySelector('h3.panel-title > a.list-title');
if (!titleA) return;
const href = titleA.getAttribute('href') || '';
if (!href.startsWith('/bt/')) return;
const hash = href.replace('/bt/', '');
if (!hash) return;
const title = titleA.textContent.trim();
if (!title.toUpperCase().replace(/[-_\s]/g, '').includes(kwNorm)) return;
const maglink = `magnet:?xt=urn:btih:${hash}`;
const infoItems = [...panel.querySelectorAll('.panel-footer .info-item')];
const size = infoItems[0]?.textContent?.trim() || '';
const date = infoItems[2]?.textContent?.trim() || '';
results.push({title, magnet: maglink, size, date});
});
return results;
}
}
function formatBytes(bytes) {
if (!bytes || bytes <= 0) return '';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let i = 0;
let size = bytes;
while (size >= 1024 && i < units.length - 1) {
size /= 1024;
i++;
}
return size.toFixed(1) + ' ' + units[i];
}
function extractCode(text) {
if (!text) return null;
const patterns = [
/FC2[-\s_]?(?:PPV)?[-\s_]?(\d{6,9})/i,
/([A-Z]{2,15})-(\d{2,10})(?:-(\d+))?/i,
/([A-Z]{2,15})-([A-Z]{0,2}\d{2,10})/i,
/^[A-Z0-9]+[-_](\d{6}[-_]\d{2,3})/i,
/(\d{6}[-_]\d{2,3})[-_][A-Z0-9]+$/i,
/(? {
const sizeDelta = parseSize(b.size) - parseSize(a.size);
if (sortMode === 'size') return sizeDelta;
const aTime = parseDate(a.date), bTime = parseDate(b.date);
if (!aTime && !bTime) return sizeDelta;
if (!aTime) return 1;
if (!bTime) return -1;
const timeDelta = sortMode === 'oldest' ? aTime - bTime : bTime - aTime;
return timeDelta || sizeDelta;
});
$container.empty();
const $table = $(``);
const $tbody = $table.find("tbody");
results.forEach((result => {
const quality = classifyQuality(result.title);
let 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 = $(`
|
|
${result.size || '未知'} |
${displayDate || '未知'} |
|
`);
$tbody.append($row);
}));
$container.append($table);
}
export {MagnetHubPlugin};