mirror of
https://github.com/fireinrain/JHS-enhance.git
synced 2026-09-28 07:36:38 +08:00
1136 lines
50 KiB
JavaScript
1136 lines
50 KiB
JavaScript
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 `
|
||
<style>
|
||
.magnet-container {
|
||
margin: 12px auto;
|
||
width: 100%;
|
||
font-family: Arial, sans-serif;
|
||
}
|
||
.magnet-tabs {
|
||
display: flex;
|
||
border-bottom: 1px solid #ddd;
|
||
margin-bottom: 0;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
.magnet-tab {
|
||
padding: 4px 10px;
|
||
cursor: pointer;
|
||
border: 1px solid transparent;
|
||
border-bottom: none;
|
||
margin-right: 3px;
|
||
background: #f5f5f5;
|
||
border-radius: 5px 5px 0 0;
|
||
font-size: 12px;
|
||
transition: background .16s;
|
||
}
|
||
.magnet-tab.active {
|
||
background: #fff;
|
||
border-color: #ddd;
|
||
border-bottom: 1px solid #fff;
|
||
margin-bottom: -1px;
|
||
font-weight: bold;
|
||
}
|
||
.magnet-tab:hover:not(.active) {
|
||
background: #e9e9e9;
|
||
}
|
||
.magnet-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 10px;
|
||
background: #fafafa;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
.magnet-sort-select {
|
||
height: 24px;
|
||
padding: 1px 18px 1px 5px;
|
||
border: 1px solid #ccc;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
color: #333;
|
||
background: #fff;
|
||
cursor: pointer;
|
||
}
|
||
.magnet-sort-select:disabled {
|
||
opacity: .5;
|
||
cursor: not-allowed;
|
||
}
|
||
.magnet-results {
|
||
min-height: 160px;
|
||
}
|
||
.magnet-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
table-layout: fixed;
|
||
font-size: 12px;
|
||
}
|
||
.magnet-table th {
|
||
padding: 4px;
|
||
background: #f8f8f8;
|
||
border-bottom: 2px solid #e0e0e0;
|
||
font-weight: 600;
|
||
color: #555;
|
||
font-size: 11px;
|
||
text-align: left;
|
||
}
|
||
.magnet-table th.col-name { max-width: 0; }
|
||
.magnet-table th.col-size { width: 60px; text-align: center; }
|
||
.magnet-table th.col-date { width: 75px; text-align: center; }
|
||
.magnet-table th.col-actions { width: 170px; text-align: center; }
|
||
.magnet-table td {
|
||
padding: 5px 4px;
|
||
border-bottom: 1px solid #eee;
|
||
vertical-align: middle;
|
||
}
|
||
.magnet-table tr:hover td {
|
||
background-color: #f9f9f9;
|
||
}
|
||
.magnet-table td.col-size { text-align: center; white-space: nowrap; }
|
||
.magnet-table td.col-date { text-align: center; white-space: nowrap; font-size: 11px; color: #888; }
|
||
.magnet-table td.col-actions { text-align: center; white-space: nowrap; }
|
||
.magnet-name-cell {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
}
|
||
.magnet-name-cell > a {
|
||
flex: 1 1 auto;
|
||
min-width: 0;
|
||
display: block;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
color: #243b67;
|
||
font-weight: 650;
|
||
text-decoration: none;
|
||
line-height: 1.4;
|
||
}
|
||
.magnet-name-cell > a:hover {
|
||
color: #1d4ed8;
|
||
text-decoration: underline;
|
||
}
|
||
.magnet-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
padding: 1px 4px;
|
||
border-radius: 3px;
|
||
font-size: 10px;
|
||
font-weight: 800;
|
||
line-height: 1.4;
|
||
color: #fff;
|
||
}
|
||
.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: 14px;
|
||
color: #666;
|
||
font-size: 12px;
|
||
}
|
||
.magnet-error {
|
||
color: #f44336;
|
||
padding: 8px;
|
||
font-size: 12px;
|
||
}
|
||
.magnet-empty {
|
||
text-align: center;
|
||
padding: 14px;
|
||
color: #999;
|
||
font-size: 12px;
|
||
}
|
||
.magnet-empty a {
|
||
color: #e74c3c;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
}
|
||
.magnet-hub-btn {
|
||
display: inline-block;
|
||
min-width: 36px;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
color: white;
|
||
font-weight: bold;
|
||
font-size: 11px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,.12);
|
||
text-shadow: 0 1px 1px rgba(0,0,0,.15);
|
||
border: none;
|
||
line-height: 1.3;
|
||
margin-left: 3px;
|
||
}
|
||
.magnet-hub-btn:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 3px 6px rgba(0,0,0,.18);
|
||
opacity: .92;
|
||
}
|
||
.magnet-hub-btn:active {
|
||
transform: translateY(0);
|
||
box-shadow: 0 1px 2px rgba(0,0,0,.1);
|
||
}
|
||
.magnet-hub-btn.copied {
|
||
background-color: #66bb6a;
|
||
}
|
||
.magnet-hub-btn.copy-btn {
|
||
background-color: #8b5cf6;
|
||
}
|
||
.magnet-hub-btn.check-btn {
|
||
background-color: #e11d48;
|
||
}
|
||
.magnet-hub-btn.down-btn {
|
||
background-color: #14b8a6;
|
||
}
|
||
.whatslink-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 2147483700;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 22px;
|
||
background: rgba(15,23,42,.66);
|
||
backdrop-filter: blur(8px);
|
||
}
|
||
.whatslink-modal {
|
||
position: relative;
|
||
width: min(1140px, calc(100vw - 36px));
|
||
max-width: 1140px;
|
||
max-height: calc(100vh - 36px);
|
||
background: transparent;
|
||
border: 0;
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||
}
|
||
.whatslink-gallery-scene {
|
||
display: grid;
|
||
grid-template-rows: minmax(0, 1fr) auto auto;
|
||
row-gap: 3px;
|
||
width: 100%;
|
||
align-items: center;
|
||
justify-items: center;
|
||
}
|
||
.whatslink-gallery-visual {
|
||
position: relative;
|
||
z-index: 1;
|
||
width: 100%;
|
||
display: grid;
|
||
place-items: center;
|
||
overflow: hidden;
|
||
}
|
||
.whatslink-gallery-hero {
|
||
width: 100%;
|
||
height: 100%;
|
||
min-height: 0;
|
||
object-fit: contain;
|
||
display: block;
|
||
}
|
||
.whatslink-gallery-close {
|
||
position: absolute;
|
||
top: 12px;
|
||
right: 14px;
|
||
z-index: 4;
|
||
width: 32px;
|
||
height: 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 0;
|
||
border-radius: 50%;
|
||
color: #fff;
|
||
background: rgba(42,45,49,.76);
|
||
cursor: pointer;
|
||
padding: 0 0 2px;
|
||
font-family: Arial, sans-serif;
|
||
font-size: 23px;
|
||
line-height: 1;
|
||
}
|
||
.whatslink-gallery-close:hover { background: rgba(22,25,28,.92); }
|
||
.whatslink-gallery-arrow {
|
||
position: absolute;
|
||
top: 50%;
|
||
z-index: 3;
|
||
width: 34px;
|
||
height: 34px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transform: translateY(-50%);
|
||
border: 0;
|
||
border-radius: 50%;
|
||
color: #fff;
|
||
background: rgba(38,42,45,.76);
|
||
cursor: pointer;
|
||
padding: 0 0 3px;
|
||
font-family: Arial, sans-serif;
|
||
font-size: 27px;
|
||
line-height: 1;
|
||
}
|
||
.whatslink-gallery-arrow:hover { background: rgba(22,25,28,.92); }
|
||
.whatslink-gallery-prev { left: 16px; }
|
||
.whatslink-gallery-next { right: 16px; }
|
||
.whatslink-gallery-info {
|
||
position: relative;
|
||
z-index: 3;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 4px 10px;
|
||
border-radius: 4px;
|
||
color: #fff;
|
||
background: rgba(38,40,42,.68);
|
||
font: 11px ui-monospace, SFMono-Regular, Consolas, monospace;
|
||
white-space: nowrap;
|
||
}
|
||
.whatslink-gallery-info span + span::before { content: " · "; margin-right: 8px; color: rgba(255,255,255,.42); }
|
||
.whatslink-gallery-thumbs {
|
||
position: relative;
|
||
z-index: 3;
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 3px;
|
||
max-width: 100%;
|
||
padding: 3px;
|
||
border-radius: 4px;
|
||
background: rgba(29,31,34,.68);
|
||
}
|
||
.whatslink-gallery-thumb {
|
||
width: 88px;
|
||
height: 50px;
|
||
padding: 0;
|
||
overflow: hidden;
|
||
border: 2px solid transparent;
|
||
border-radius: 3px;
|
||
background: transparent;
|
||
cursor: pointer;
|
||
}
|
||
.whatslink-gallery-thumb.active { border-color: #e3a05b; }
|
||
.whatslink-gallery-thumb img { width: 100%; height: 100%; display: block; object-fit: cover; }
|
||
.whatslink-gallery-empty {
|
||
position: absolute;
|
||
z-index: 2;
|
||
display: none;
|
||
width: min(420px, 72%);
|
||
text-align: center;
|
||
color: rgba(255,255,255,.74);
|
||
}
|
||
.whatslink-modal.no-shots .whatslink-gallery-empty { display: block; }
|
||
.whatslink-modal.no-shots .whatslink-gallery-visual { height: min(460px, calc(100vh - 100px)); }
|
||
.whatslink-modal.no-shots .whatslink-gallery-hero,
|
||
.whatslink-modal.no-shots .whatslink-gallery-arrow,
|
||
.whatslink-modal.no-shots .whatslink-gallery-thumbs { display: none; }
|
||
.whatslink-gallery-empty-icon {
|
||
width: 56px;
|
||
height: 56px;
|
||
margin: 0 auto 14px;
|
||
display: grid;
|
||
place-items: center;
|
||
border: 1px solid rgba(255,255,255,.24);
|
||
border-radius: 50%;
|
||
color: #e3a05b;
|
||
font-size: 24px;
|
||
}
|
||
.whatslink-gallery-empty-title { margin-bottom: 7px; color: #fff; font-size: 17px; font-weight: 800; }
|
||
.whatslink-gallery-empty-text { margin: 0; font-size: 13px; line-height: 1.6; }
|
||
.whatslink-loading { padding: 28px; text-align: center; color: #475569; font-size: 14px; }
|
||
</style>
|
||
`;
|
||
}
|
||
|
||
createMagnetHub(keyword) {
|
||
keyword = keyword.replace("FC2-", "");
|
||
const $container = $('<div class="magnet-container"></div>');
|
||
const $tabs = $('<div class="magnet-tabs"></div>');
|
||
const key = "jhs_magnetHub_selectedEngine";
|
||
const savedEngineId = localStorage.getItem(key);
|
||
let defaultEngineIndex = 0;
|
||
const $tabBox = $('<div style="display: flex; align-items: center;"></div>');
|
||
this.searchEngines.forEach(((engine, index) => {
|
||
const $tab = $(`<div class="magnet-tab" data-engine="${engine.id}">${engine.name}</div>`);
|
||
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 = $(`<select class="magnet-sort-select" disabled style="margin-left:auto;margin-right:4px;">
|
||
<option value="size" ${sortMode === "size" ? "selected" : ""}>按大小</option>
|
||
<option value="newest" ${sortMode === "newest" ? "selected" : ""}>按最新</option>
|
||
<option value="oldest" ${sortMode === "oldest" ? "selected" : ""}>按最旧</option>
|
||
</select>`);
|
||
$tabBox.append($sortSelect);
|
||
$tabs.append($tabBox);
|
||
$tabs.append(`<a style="margin-right: 20px;margin-top:3px" id="targetBox" href="${this.currentEngine.targetPage.replace("{keyword}", encodeURIComponent(keyword))}" target="_blank">原网页</a>`);
|
||
$container.append($tabs);
|
||
|
||
const $resultsContainer = $('<div class="magnet-results"></div>');
|
||
$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(`<div class="magnet-loading">正在从 ${engine.name} 搜索 "${keyword}"...</div>`);
|
||
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(`<div class="magnet-error">${message} <a id="retrySearch" href="#">🔄 刷新</a></div>`);
|
||
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 = $(`<table class="magnet-table">
|
||
<thead><tr>
|
||
<th class="col-name">名称</th>
|
||
<th class="col-size">大小</th>
|
||
<th class="col-date">日期</th>
|
||
<th class="col-actions">操作</th>
|
||
</tr></thead>
|
||
<tbody></tbody>
|
||
</table>`);
|
||
const $tbody = $table.find("tbody");
|
||
results.forEach((result => {
|
||
const quality = classifyQuality(result.title);
|
||
let badges = '';
|
||
if (quality.is4K) badges += '<span class="magnet-badge mg-four-k">4K</span>';
|
||
if (quality.isChinese) badges += '<span class="magnet-badge mg-subtitle">中字</span>';
|
||
if (quality.isCracked) badges += '<span class="magnet-badge mg-cracked">破解</span>';
|
||
const displayDate = formatMagnetDate(result.date);
|
||
const dnCode = extractCode(result.title);
|
||
const $row = $(`<tr>
|
||
<td>
|
||
<div class="magnet-name-cell">
|
||
${badges}
|
||
<a href="${result.magnet}" title="${result.title}">${result.title}</a>
|
||
</div>
|
||
</td>
|
||
<td class="col-size">${result.size || '未知'}</td>
|
||
<td class="col-date">${displayDate || '未知'}</td>
|
||
<td class="col-actions">
|
||
<button class="magnet-hub-btn copy-btn" data-magnet="${result.magnet}" data-dncode="${dnCode || ''}">复制</button>
|
||
<button class="magnet-hub-btn check-btn" data-magnet="${result.magnet}">验车</button>
|
||
<button class="magnet-hub-btn down-btn down-115" data-magnet="${result.magnet}">115离线</button>
|
||
</td>
|
||
</tr>`);
|
||
$tbody.append($row);
|
||
}));
|
||
$container.append($table);
|
||
} else {
|
||
$container.append(`<div class="magnet-empty">无搜索结果 <a id="retrySearch" href="#">🔄 刷新</a></div>`);
|
||
$("#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 = '<div class="whatslink-modal no-shots"><div class="whatslink-loading">正在验车...</div></div>';
|
||
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 = `<div class="whatslink-gallery-scene">
|
||
<div class="whatslink-gallery-visual">
|
||
<button class="whatslink-gallery-close" type="button" aria-label="关闭">×</button>
|
||
<button class="whatslink-gallery-arrow whatslink-gallery-prev" type="button" aria-label="上一张">‹</button>
|
||
<img class="whatslink-gallery-hero" alt="截图预览">
|
||
<button class="whatslink-gallery-arrow whatslink-gallery-next" type="button" aria-label="下一张">›</button>
|
||
<div class="whatslink-gallery-empty">
|
||
<div class="whatslink-gallery-empty-icon">?</div>
|
||
<div class="whatslink-gallery-empty-title">暂无截图</div>
|
||
<p class="whatslink-gallery-empty-text">当前资源没有可展示的截图,可以结合资源名称和文件数量判断。</p>
|
||
</div>
|
||
</div>
|
||
<div class="whatslink-gallery-info">
|
||
<span class="whatslink-gallery-name"></span>
|
||
<span class="whatslink-gallery-count"></span>
|
||
<span class="whatslink-gallery-index"></span>
|
||
</div>
|
||
<div class="whatslink-gallery-thumbs"></div>
|
||
</div>`;
|
||
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 = `<img src="${url}" alt="截图${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) {
|
||
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,
|
||
/(?<!\w)(\d{6}[-_]\d{2,3})(?!\w)/,
|
||
/([A-Z]{1,2})(\d{3,4})/i,
|
||
];
|
||
for (const re of patterns) {
|
||
const m = text.match(re);
|
||
if (m) return m[0].toUpperCase();
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function randomString(length) {
|
||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||
let result = '';
|
||
for (let i = 0; i < length; i++) result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||
return result;
|
||
}
|
||
|
||
function RerenderSort($container, sortMode) {
|
||
const data = $container.data("_magnetData");
|
||
if (!data || !data.length) return;
|
||
const results = [...data].sort((a, b) => {
|
||
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 = $(`<table class="magnet-table">
|
||
<thead><tr>
|
||
<th class="col-name">名称</th>
|
||
<th class="col-size">大小</th>
|
||
<th class="col-date">日期</th>
|
||
<th class="col-actions">操作</th>
|
||
</tr></thead>
|
||
<tbody></tbody>
|
||
</table>`);
|
||
const $tbody = $table.find("tbody");
|
||
results.forEach((result => {
|
||
const quality = classifyQuality(result.title);
|
||
let badges = '';
|
||
if (quality.is4K) badges += '<span class="magnet-badge mg-four-k">4K</span>';
|
||
if (quality.isChinese) badges += '<span class="magnet-badge mg-subtitle">中字</span>';
|
||
if (quality.isCracked) badges += '<span class="magnet-badge mg-cracked">破解</span>';
|
||
const displayDate = formatMagnetDate(result.date);
|
||
const dnCode = extractCode(result.title);
|
||
const $row = $(`<tr>
|
||
<td>
|
||
<div class="magnet-name-cell">
|
||
${badges}
|
||
<a href="${result.magnet}" title="${result.title}">${result.title}</a>
|
||
</div>
|
||
</td>
|
||
<td class="col-size">${result.size || '未知'}</td>
|
||
<td class="col-date">${displayDate || '未知'}</td>
|
||
<td class="col-actions">
|
||
<button class="magnet-hub-btn copy-btn" data-magnet="${result.magnet}" data-dncode="${dnCode || ''}">复制</button>
|
||
<button class="magnet-hub-btn check-btn" data-magnet="${result.magnet}">验车</button>
|
||
<button class="magnet-hub-btn down-btn down-115" data-magnet="${result.magnet}">115</button>
|
||
</td>
|
||
</tr>`);
|
||
$tbody.append($row);
|
||
}));
|
||
$container.append($table);
|
||
}
|
||
|
||
export {MagnetHubPlugin}; |