// ==UserScript== // @name iwara & hanime1 视频比对 // @namespace http://tampermonkey.net/ // @version 1.7 // @description 标题词干匹配 + 时长精确过滤 + 可调节秒数容差 + iwara池排序 // @author bydbot+trae,mimo 2 pro // @match https://www.iwara.tv/* // @include https://*.hanime*.*/search?* // @include https://hanime*.*/search?* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @license GPL-3.0 // @downloadURL none // ==/UserScript== (function() { 'use strict'; GM_addStyle(` #matcher-menu-btn { position: fixed; right: 20px; bottom: 20px; background: linear-gradient(135deg, #00ccff, #0066ff); color: #fff; width: 50px; height: 50px; border-radius: 50%; cursor: pointer; z-index: 99999; font-size: 24px; font-weight: bold; box-shadow: 0 4px 15px rgba(0,102,255,0.4); display: flex; align-items: center; justify-content: center; transition: all 0.3s; user-select: none; border: 2px solid rgba(255,255,255,0.2); } #matcher-menu-btn:hover { transform: scale(1.1); box-shadow: 0 6px 20px rgba(0,102,255,0.6); background: linear-gradient(135deg, #0066ff, #00ccff); } #matcher-dropdown { position: fixed; right: 20px; bottom: 80px; background: #1e1e1e; border: 1px solid #444; border-radius: 8px; z-index: 99998; display: none; overflow: hidden; box-shadow: 0 8px 24px rgba(0,0,0,0.6); min-width: 180px; } #matcher-dropdown.show { display: block; } .dropdown-item { padding: 12px 18px; cursor: pointer; font-size: 13px; color: #eee; transition: background 0.2s; white-space: nowrap; border-bottom: 1px solid #2a2a2a; font-family: sans-serif; } .dropdown-item:last-child { border-bottom: none; } .dropdown-item:hover { background: #2a2a2a; color: #00ccff; } #matcher-panel, #sort-panel { position: fixed; display: none; flex-direction: column; background: #1a1a1a; color: #eee; z-index: 10000; padding: 12px; border-radius: 10px; font-family: sans-serif; box-shadow: 0 10px 30px rgba(0,0,0,0.7); border: 1px solid #333; overflow: hidden; resize: both; } #matcher-panel { min-width: 900px; max-width: 95vw; min-height: 500px; max-height: 90vh; } #sort-panel { min-width: 900px; max-width: 95vw; min-height: 500px; max-height: 90vh; } .panel-header { border-bottom: 1px solid #333; padding: 8px 8px 8px 12px; margin: -12px -12px 10px -12px; background: #222; border-radius: 10px 10px 0 0; display: flex; justify-content: space-between; align-items: center; } .panel-header h3 { margin: 0; font-size: 14px; color: #00ccff; } .close-btn { cursor: pointer; color: #888; font-size: 18px; padding: 0 8px; } .close-btn:hover { color: #ff4444; } .stat-banner { display: flex; justify-content: space-around; background: #252525; padding: 6px; border-radius: 6px; margin-bottom: 10px; font-size: 11px; border: 1px solid #333; } .stat-item b { color: #00ffcc; } .progress-container { width: 100%; height: 4px; background: #333; border-radius: 2px; margin-bottom: 10px; overflow: hidden; display: none; } #progress-bar { width: 0%; height: 100%; background: linear-gradient(90deg, #00ccff, #00ffcc); transition: width 0.1s; } .filter-controls { background: #252525; border-radius: 6px; padding: 10px; margin-bottom: 10px; border: 1px solid #333; display: flex; justify-content: space-between; align-items: center; } .filter-item { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; } .filter-item label { width: 80px; font-size: 11px; color: #aaa; } .filter-item input[type=number] { width: 70px; background: #333; color: #fff; border: 1px solid #555; border-radius: 4px; padding: 4px 6px; font-size: 11px; } .filter-item span { font-size: 10px; color: #666; } .filter-hint { font-size: 10px; color: #ffaa00; text-align: center; margin-top: 5px; } .swap-mode-btn { background: #3a3a3a; color: #fff; border: 1px solid #444; padding: 6px 12px; border-radius: 5px; font-size: 11px; cursor: pointer; transition: 0.2s; white-space: nowrap; margin-left: 10px; } .swap-mode-btn:hover { background: #4a4a4a; border-color: #00ccff; } .swap-mode-btn.active { background: #00ccff; color: #000; border-color: #00ccff; box-shadow: 0 0 10px rgba(0,204,255,0.5); } .column-swap-ready { cursor: pointer; box-shadow: 0 0 15px rgba(0,204,255,0.6); border-color: #00ccff !important; } .column-swap-selected { cursor: pointer; box-shadow: 0 0 20px rgba(255,170,0,0.8); border-color: #ffaa00 !important; background: rgba(255,170,0,0.1); } .threshold-wrap { display: flex; align-items: center; gap: 10px; font-size: 12px; margin-bottom: 10px; color: #aaa; } input[type=range] { flex: 1; cursor: pointer; } .compare-container { display: flex; gap: 10px; margin-top: 10px; flex: 1; min-height: 200px; overflow: hidden; } .compare-column { flex: 1; display: flex; flex-direction: column; background: #000; border-radius: 6px; padding: 6px; border: 1px solid #222; overflow: hidden; min-width: 0; position: relative; } .compare-column-placeholder { flex: 1; min-height: 200px; background: #000; border-radius: 6px; border: 1px solid #222; } #matched-column { background: #0a1a1a; border: 1px solid #00ff88; } .column-header { font-size: 11px; font-weight: bold; color: #888; border-bottom: 1px solid #333; margin-bottom: 8px; padding: 4px; display: flex; justify-content: space-between; flex-shrink: 0; user-select: none; cursor: pointer; } .column-header:hover { color: #00ccff; background: rgba(0,204,255,0.1); border-radius: 4px; } .diff-count { color: #ff4444; font-size: 12px; } .list-content { flex: 1; overflow-y: auto; overflow-x: hidden; font-size: 11px; min-height: 100px; scrollbar-width: thin; scrollbar-color: #444 #1a1a1a; } .list-content::-webkit-scrollbar { width: 6px; } .list-content::-webkit-scrollbar-track { background: #1a1a1a; } .list-content::-webkit-scrollbar-thumb { background: #444; border-radius: 3px; } .list-content::-webkit-scrollbar-thumb:hover { background: #666; } .diff-item { border-bottom: 1px solid #1a1a1a; padding: 8px 5px; transition: 0.2s; display: flex; align-items: center; justify-content: space-between; } .diff-item:hover { background: #222; } .item-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .item-title a { color: #5cafff; text-decoration: none; } .duration-badge { font-size: 10px; margin-left: 10px; flex-shrink: 0; font-family: monospace; background: #333; padding: 2px 6px; border-radius: 12px; } .duration-iwara { color: #ffaa00; border-left: 2px solid #ffaa00; } .duration-hanime { color: #00ffaa; border-left: 2px solid #00ffaa; } .btn-row { display: flex; gap: 6px; margin-bottom: 10px; } button { flex: 1; cursor: pointer; background: #2a2a2a; color: #fff; border: 1px solid #444; padding: 8px; border-radius: 5px; font-size: 11px; transition: 0.2s; } button:hover { background: #3a3a3a; border-color: #00ccff; } .btn-capture { background: #004a80 !important; font-weight: bold; } .similarity-badge { display: inline-block; background: #333; color: #ffaa00; font-size: 10px; padding: 2px 5px; border-radius: 10px; margin-left: 5px; } #similarity-tooltip { position: fixed; background: #252525; border: 1px solid #444; border-radius: 6px; padding: 8px; z-index: 10001; min-width: 250px; box-shadow: 0 4px 15px rgba(0,0,0,0.5); pointer-events: none; } /* 排序面板样式 */ .sort-controls { display: flex; gap: 10px; margin-bottom: 10px; align-items: center; } .sort-controls select, .sort-controls button { background: #333; color: #fff; border: 1px solid #555; border-radius: 4px; padding: 6px 10px; font-size: 12px; cursor: pointer; } .sort-controls select:focus { border-color: #00ccff; outline: none; } .sort-list { flex: 1; overflow-y: auto; min-height: 200px; } .sort-item { border-bottom: 1px solid #1a1a1a; padding: 8px 6px; display: flex; align-items: center; gap: 8px; font-size: 11px; transition: 0.2s; } .sort-item:hover { background: #222; } .sort-rank { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: bold; flex-shrink: 0; } .sort-rank.top1 { background: #ffd700; color: #000; } .sort-rank.top2 { background: #c0c0c0; color: #000; } .sort-rank.top3 { background: #cd7f32; color: #fff; } .sort-rank.normal { background: #333; color: #888; } .sort-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .sort-title a { color: #5cafff; text-decoration: none; } .sort-meta { display: flex; gap: 8px; flex-shrink: 0; align-items: center; } .sort-badge { font-size: 10px; padding: 2px 6px; border-radius: 10px; font-family: monospace; white-space: nowrap; } .sort-badge.view { background: #1a3a2a; color: #00ff88; } .sort-badge.like { background: #3a1a2a; color: #ff6b9d; } .sort-badge.ratio { background: #2a2a1a; color: #ffcc00; } .sort-badge.date { background: #1a2a3a; color: #6bc5ff; } .sort-level-row { display: flex; align-items: center; gap: 6px; padding: 4px 8px; background: #2a2a2a; border-radius: 4px; font-size: 11px; } .sort-level-row select { background: #333; color: #fff; border: 1px solid #555; border-radius: 3px; padding: 4px 6px; font-size: 11px; } .sort-level-row label { color: #888; font-size: 10px; white-space: nowrap; } .sort-levels { display: flex; flex-direction: column; gap: 4px; margin-bottom: 8px; } .sort-levels .level-label { font-size: 11px; color: #aaa; margin-bottom: 2px; font-weight: bold; } .sort-body { display: flex; gap: 12px; flex: 1; min-height: 0; overflow: hidden; } .sort-left { width: 320px; flex-shrink: 0; display: flex; flex-direction: column; overflow-y: auto; } .sort-right { flex: 1; display: none; flex-direction: column; min-width: 0; border-left: 1px solid #333; padding-left: 12px; overflow: hidden; } .sort-right.show { display: flex; } .sort-right-header { font-size: 12px; font-weight: bold; color: #00ccff; padding: 6px 0; border-bottom: 1px solid #333; margin-bottom: 6px; display: flex; justify-content: space-between; flex-shrink: 0; } .sort-col-toggles { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 6px; flex-shrink: 0; } .sort-col-toggle { font-size: 10px; padding: 3px 8px; border-radius: 10px; cursor: pointer; user-select: none; border: 1px solid #555; background: #2a2a2a; color: #888; transition: 0.2s; } .sort-col-toggle.on { color: #fff; } .sort-col-toggle.on.view { background: #1a3a2a; border-color: #00ff88; color: #00ff88; } .sort-col-toggle.on.like { background: #3a1a2a; border-color: #ff6b9d; color: #ff6b9d; } .sort-col-toggle.on.ratio { background: #2a2a1a; border-color: #ffcc00; color: #ffcc00; } .sort-col-toggle.on.date { background: #1a2a3a; border-color: #6bc5ff; color: #6bc5ff; } .sort-col-toggle.on.duration { background: #3a2a1a; border-color: #ffaa00; color: #ffaa00; } `); // --- 工具函数 --- function parseDuration(s) { if (!s) return null; const m = s.match(/(\d+):(\d+)(?::(\d+))?/); if (!m) return null; const a = parseInt(m[1]), b = parseInt(m[2]), c = m[3] ? parseInt(m[3]) : 0; return c > 0 ? c * 3600 + a * 60 + b : a * 60 + b; } function formatDuration(sec) { if (sec == null) return '无时长'; const h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = sec % 60; const pad = n => String(n).padStart(2, '0'); return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${m}:${pad(s)}`; } function parseCompactNumber(s) { if (!s) return 0; s = s.trim().replace(/,/g, ''); const m = s.match(/^([\d.]+)\s*([KkMmBb]?)$/); if (!m) return parseInt(s) || 0; const n = parseFloat(m[1]); const suf = m[2].toUpperCase(); if (suf === 'K') return Math.round(n * 1000); if (suf === 'M') return Math.round(n * 1000000); if (suf === 'B') return Math.round(n * 1000000000); return Math.round(n); } function formatNumber(n) { if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'; if (n >= 1000) return (n / 1000).toFixed(1) + 'K'; return String(n); } function parseDate(s) { if (!s) return 0; const d = new Date(s); return isNaN(d.getTime()) ? 0 : d.getTime(); } function getPoolSize() { const iwara = JSON.stringify(GM_getValue('iwara_pool', []) || []); const hanime = JSON.stringify(GM_getValue('hanime1me_pool', []) || []); return iwara.length + hanime.length; } function formatSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / 1048576).toFixed(2) + ' MB'; } function stemWord(w) { if (/^\d+$/.test(w)) return w; return w.toLowerCase().replace(/(ing|ed|s|es|ies|ly|er|est|tion|ive|able|ible|al|y)$/, '').replace(/[^\w\u4e00-\u9fa5]/g, ''); } function cleanTitle(t) { return (t || '').replace(/\[.*?\]|\(.*?\)|【.*?】/g, '').replace(/[::]/g, ' ') .replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|\u200d|\uFE0F/g, '') .replace(/[^\w\s\u4e00-\u9fa5]/gi, ' ').replace(/\s+/g, ' ').trim().toLowerCase(); } function editDist(a, b) { const c = []; for (let i = 0; i <= a.length; i++) { let last = i; for (let j = 0; j <= b.length; j++) { if (i === 0) c[j] = j; else if (j > 0) { let nv = c[j - 1]; if (a[i - 1] !== b[j - 1]) nv = Math.min(nv, last, c[j]) + 1; c[j - 1] = last; last = nv; } } if (i > 0) c[b.length] = last; } return c[b.length]; } function charSim(a, b) { if (a.length < b.length) [a, b] = [b, a]; return a.length === 0 ? 1 : (a.length - editDist(a, b)) / a.length; } function titleSim(s1, s2) { const w1 = s1.split(/\s+/).filter(Boolean).map(stemWord); const w2 = s2.split(/\s+/).filter(Boolean).map(stemWord); if (!w1.length || !w2.length) return charSim(s1, s2); const set1 = new Set(w1), set2 = new Set(w2); let inter = 0; for (const w of set1) if (set2.has(w)) inter++; const union = set1.size + set2.size - inter; const jaccard = union > 0 ? inter / union : 0; const cov1 = set1.size > 0 ? inter / set1.size : 0; const cov2 = set2.size > 0 ? inter / set2.size : 0; return jaccard * 0.4 + (cov1 + cov2) / 2 * 0.6; } function findTopSimilar(item, pool, thres, maxN = 3) { return pool.map(op => ({ title: op.title, url: op.url, duration: op.duration, seconds: op.seconds, titleSim: titleSim(cleanTitle(item.title), cleanTitle(op.title)), durationDiff: Math.abs(item.seconds - op.seconds) })).filter(s => s.titleSim >= thres * 0.6).sort((a, b) => b.titleSim - a.titleSim).slice(0, maxN); } // --- UI --- let menuBtn, dropdown, panel, sortPanel, progressBar, progressContainer, thresholdInput, thresholdVal, toleranceInput; function createMenuButton() { menuBtn = document.createElement('div'); menuBtn.id = 'matcher-menu-btn'; menuBtn.innerHTML = '\u26A1'; menuBtn.title = '打开菜单'; document.body.appendChild(menuBtn); dropdown = document.createElement('div'); dropdown.id = 'matcher-dropdown'; dropdown.innerHTML = `
`; document.body.appendChild(dropdown); menuBtn.addEventListener('click', (e) => { e.stopPropagation(); dropdown.classList.toggle('show'); if (dropdown.classList.contains('show')) { document.getElementById('dropdown-size').textContent = formatSize(getPoolSize()); } }); document.addEventListener('click', (e) => { if (!dropdown.contains(e.target) && e.target !== menuBtn) dropdown.classList.remove('show'); }); document.getElementById('menu-save').addEventListener('click', () => { dropdown.classList.remove('show'); capturePage(); updateStats(); if (sortPanel?.style.display === 'flex') updateSortStats(); }); document.getElementById('menu-clear').addEventListener('click', () => { dropdown.classList.remove('show'); clearPools(); }); document.getElementById('menu-compare').addEventListener('click', () => { dropdown.classList.remove('show'); showComparePanel(); }); document.getElementById('menu-sort').addEventListener('click', () => { dropdown.classList.remove('show'); showSortPanel(); updateSortStats(); }); } function showComparePanel() { if (sortPanel) sortPanel.style.display = 'none'; if (!panel) initPanel(); const visible = panel.style.display === 'flex'; panel.style.display = visible ? 'none' : 'flex'; if (!visible) { setTimeout(() => centerEl(panel), 10); updateStats(); } } function showSortPanel() { if (panel) panel.style.display = 'none'; if (!sortPanel) initSortPanel(); const visible = sortPanel.style.display === 'flex'; sortPanel.style.display = visible ? 'none' : 'flex'; if (!visible) setTimeout(() => centerEl(sortPanel), 10); } function centerEl(el) { if (!el) return; const pw = el.offsetWidth || 600, ph = el.offsetHeight || 500; let left = (window.innerWidth - pw) / 2, top = (window.innerHeight - ph) / 2; left = Math.max(10, Math.min(left, window.innerWidth - pw - 10)); top = Math.max(10, Math.min(top, window.innerHeight - ph - 10)); Object.assign(el.style, { left: left + 'px', top: top + 'px', right: 'auto', bottom: 'auto' }); if (ph > window.innerHeight - 20) { el.style.height = (window.innerHeight - 20) + 'px'; el.style.top = '10px'; } } function getColumnWidth() { const container = document.getElementById('compare-container'); const vis = Array.from(container.querySelectorAll('.compare-column')).filter(c => c.style.display !== 'none'); return (container.offsetWidth - (vis.length - 1) * 10) / vis.length; } function toggleColumnExpand(colId, listId) { const col = document.getElementById(colId); if (!col) return; if (col.classList.contains('expanded')) { col.classList.remove('expanded'); document.getElementById(colId + '-placeholder')?.remove(); col.style.cssText = ''; } else { const w = getColumnWidth(); const ph = document.createElement('div'); ph.id = colId + '-placeholder'; ph.className = 'compare-column-placeholder'; ph.style.width = ph.style.minWidth = ph.style.maxWidth = w + 'px'; col.parentNode.insertBefore(ph, col.nextSibling); col.classList.add('expanded'); } updateColumnPositions(); } function updateColumnPositions() { const container = document.getElementById('compare-container'); const vis = Array.from(container.querySelectorAll('.compare-column')).filter(c => c.style.display !== 'none'); const cw = getColumnWidth(); const cwPercent = (cw / container.offsetWidth) * 100; vis.forEach(col => { const ph = document.getElementById(col.id + '-placeholder'); if (ph) ph.style.width = ph.style.minWidth = ph.style.maxWidth = cw + 'px'; }); vis.forEach(col => { if (!col.classList.contains('expanded')) { col.style.position = col.style.left = col.style.right = col.style.top = col.style.height = col.style.zIndex = col.style.background = col.style.boxShadow = col.style.border = col.style.width = ''; } }); const borders = { 'iwara-column': '#ffffffff', 'hanime1me-column': '#ff0000ff', 'matched-column': '#00ff88' }; vis.forEach((col, i) => { if (col.classList.contains('expanded')) { const leftPct = i * (cwPercent + (10 / container.offsetWidth) * 100); Object.assign(col.style, { position: 'absolute', top: '45px', height: 'calc(100% - 57px)', zIndex: '10', background: 'rgba(0,0,0,0.95)', boxShadow: '0 0 30px rgba(0,204,255,0.3)', border: '2px solid ' + (borders[col.id] || '#fff'), left: leftPct + '%', width: cwPercent + '%' }); } }); } // --- 初始化对比面板 --- function initPanel() { panel = document.createElement('div'); panel.id = 'matcher-panel'; const th = GM_getValue('match_threshold', 0.5); const tol = GM_getValue('duration_tolerance', 10); panel.innerHTML = `