// ==UserScript==
// @name iwara & hanime1 视频比对
// @namespace http://tampermonkey.net/
// @version 1.3
// @description 标题词干匹配 + 时长精确过滤 + 可调节秒数容差
// @author bydbot+trae
// @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-toggle-btn {
position: fixed;
background: linear-gradient(135deg, #00ccff, #0066ff);
color: white;
width: 50px;
height: 50px;
border-radius: 50%;
cursor: pointer;
z-index: 99999;
box-shadow: 0 4px 15px rgba(0,102,255,0.4);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: bold;
transition: all 0.3s ease;
user-select: none;
border: 2px solid rgba(255,255,255,0.2);
}
#matcher-toggle-btn:hover {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(0,102,255,0.6);
background: linear-gradient(135deg, #0066ff, #00ccff);
}
#matcher-panel {
position: fixed;
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;
display: none;
flex-direction: column;
min-width: 900px;
max-width: 95vw;
min-height: 500px;
max-height: 90vh;
overflow: hidden;
backdrop-filter: blur(5px);
resize: both;
}
.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;
line-height: 1;
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;
flex-shrink: 0;
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;
transition: none;
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;
}
.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;
}
.diff-item {
border-bottom: 1px solid #1a1a1a;
padding: 8px 5px;
transition: 0.2s;
position: relative;
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;
}
.item-duration-iwara {
color: #ffaa00;
font-size: 10px;
margin-left: 10px;
flex-shrink: 0;
font-family: monospace;
background: #333;
padding: 2px 6px;
border-radius: 12px;
border-left: 2px solid #ffaa00;
}
.item-duration-hanime1 {
color: #00ffaa;
font-size: 10px;
margin-left: 10px;
flex-shrink: 0;
font-family: monospace;
background: #333;
padding: 2px 6px;
border-radius: 12px;
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;
}
/* 滚动条样式 */
.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;
}
`);
// --- 工具函数:解析时长字符串为秒数 ---
function parseDuration(durationStr) {
if (!durationStr) return null;
const timeMatch = durationStr.match(/(\d+):(\d+)(?::(\d+))?/);
if (timeMatch) {
const minutes = parseInt(timeMatch[1]);
const seconds = parseInt(timeMatch[2]);
const hours = timeMatch[3] ? parseInt(timeMatch[3]) : 0;
if (hours > 0) {
return hours * 3600 + minutes * 60 + seconds;
} else {
return minutes * 60 + seconds;
}
}
return null;
}
// --- 格式化时长显示 ---
function formatDuration(seconds) {
if (seconds === null || seconds === undefined) return '无时长';
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
} else {
return `${minutes}:${secs.toString().padStart(2, '0')}`;
}
}
// --- 检查时长是否匹配 ---
function isDurationMatch(dur1, dur2, tolerance) {
if (dur1 === null || dur2 === null) return false;
return Math.abs(dur1 - dur2) <= tolerance;
}
// --- 词干提取 ---
function stemWord(word) {
if (/^\d+$/.test(word)) {
return word;
}
return word
.toLowerCase()
.replace(/(ing|ed|s|es|ies|ly|er|est|tion|ive|able|ible|al|y)$/, '')
.replace(/[^\w\u4e00-\u9fa5]/g, '');
}
// --- 清洗标题 ---
function cleanTitle(title) {
if (!title) return "";
return title
.replace(/\[.*?\]/g, '')
.replace(/\(.*?\)/g, '')
.replace(/【.*?】/g, '')
.replace(/[::]/g, ' ')
.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|\u200d|\uFE0F/g, '')
.replace(/[^\w\s\u4e00-\u9fa5]/gi, ' ')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
}
// --- 标题相似度 (词干匹配) ---
function titleSimilarity(s1, s2) {
const words1 = s1.split(/\s+/).filter(w => w.length > 0).map(stemWord);
const words2 = s2.split(/\s+/).filter(w => w.length > 0).map(stemWord);
if (words1.length === 0 || words2.length === 0) {
return charSimilarity(s1, s2);
}
const set1 = new Set(words1);
const set2 = new Set(words2);
let intersection = 0;
for (const word of set1) {
if (set2.has(word)) intersection++;
}
const union = set1.size + set2.size - intersection;
const jaccard = union > 0 ? intersection / union : 0;
const coverage1 = set1.size > 0 ? intersection / set1.size : 0;
const coverage2 = set2.size > 0 ? intersection / set2.size : 0;
return jaccard * 0.4 + (coverage1 + coverage2) / 2 * 0.6;
}
// --- 字符级相似度 ---
function charSimilarity(s1, s2) {
let longer = s1, shorter = s2;
if (s1.length < s2.length) [longer, shorter] = [shorter, longer];
if (longer.length === 0) return 1.0;
const distance = editDistance(longer, shorter);
return (longer.length - distance) / longer.length;
}
function editDistance(s1, s2) {
let costs = [];
for (let i = 0; i <= s1.length; i++) {
let lastValue = i;
for (let j = 0; j <= s2.length; j++) {
if (i === 0) costs[j] = j;
else if (j > 0) {
let newValue = costs[j - 1];
if (s1.charAt(i - 1) !== s2.charAt(j - 1))
newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1;
costs[j - 1] = lastValue;
lastValue = newValue;
}
}
if (i > 0) costs[s2.length] = lastValue;
}
return costs[s2.length];
}
// --- UI 元素 ---
let toggleBtn, panel, progressBar, progressContainer;
let thresholdInput, thresholdVal;
let toleranceInput;
// --- 创建唤出按钮 ---
function createToggleButton() {
console.log('[iwara & hanime1me] 开始创建按钮...');
toggleBtn = document.createElement('div');
toggleBtn.id = 'matcher-toggle-btn';
toggleBtn.innerHTML = '⚡';
toggleBtn.title = '打开比对面板';
toggleBtn.style.position = 'fixed';
toggleBtn.style.zIndex = '99999';
toggleBtn.style.right = '20px';
toggleBtn.style.bottom = '20px';
console.log('[iwara & hanime1me] 按钮元素已创建,准备添加到页面');
document.body.appendChild(toggleBtn);
console.log('[iwara & hanime1me] 按钮已添加到页面');
toggleBtn.addEventListener('click', (e) => {
console.log('[iwara & hanime1me] 按钮被点击');
showPanel();
});
console.log('[iwara & hanime1me] 按钮创建完成');
}
// --- 显示面板 ---
function showPanel() {
if (!panel) {
initPanel();
}
if (panel.style.display === 'flex') {
// 隐藏面板
panel.style.display = 'none';
toggleBtn.innerHTML = '⚡';
toggleBtn.title = '打开比对面板';
} else {
// 显示面板
panel.style.display = 'flex';
toggleBtn.innerHTML = '×';
toggleBtn.title = '关闭比对面板';
// 强制浏览器计算尺寸
setTimeout(() => {
centerPanel();
}, 10);
updateStats();
}
}
// --- 切换列扩展 ---
function toggleColumnExpand(columnId, listId) {
const column = document.getElementById(columnId);
const listContent = document.getElementById(listId);
if (!column || !listContent) return;
const placeholderId = columnId + '-placeholder';
// 获取所有可见列
const container = document.getElementById('compare-container');
const allColumns = Array.from(container.querySelectorAll('.compare-column'));
const visibleColumns = allColumns.filter(col => col.style.display !== 'none');
const columnCount = visibleColumns.length;
// 获取容器实际宽度
const containerWidth = container.offsetWidth;
// 总间距 = (列数 - 1) * 10px
const totalGap = (columnCount - 1) * 10;
// 每列的实际宽度(像素)
const columnPixelWidth = (containerWidth - totalGap) / columnCount;
if (column.classList.contains('expanded')) {
// 收缩:移除占位元素
column.classList.remove('expanded', 'iwara-expanded', 'hanime-expanded', 'matched-expanded');
const placeholder = document.getElementById(placeholderId);
if (placeholder) placeholder.remove();
// 清除展开时设置的样式
column.style.position = '';
column.style.left = '';
column.style.right = '';
column.style.width = '';
column.style.top = '';
column.style.height = '';
column.style.zIndex = '';
column.style.background = '';
column.style.boxShadow = '';
column.style.border = '';
// 重新布局所有列
updateColumnPositions();
} else {
// 每个展开的列都需要有自己的占位符
// 检查当前列是否已经有占位符
const existingPlaceholder = document.getElementById(placeholderId);
if (!existingPlaceholder) {
// 创建占位元素 - 精确设置宽度以保持布局
const placeholder = document.createElement('div');
placeholder.id = placeholderId;
placeholder.className = 'compare-column-placeholder';
// 使用精确的像素宽度而不是 flex: 1
placeholder.style.width = columnPixelWidth + 'px';
placeholder.style.minWidth = columnPixelWidth + 'px';
placeholder.style.maxWidth = columnPixelWidth + 'px';
placeholder.style.minHeight = '200px';
placeholder.style.background = '#000';
placeholder.style.borderRadius = '6px';
placeholder.style.border = '1px solid #222';
placeholder.style.flexShrink = '0'; // 防止被压缩
column.parentNode.insertBefore(placeholder, column.nextSibling);
}
// 扩展当前列
column.classList.add('expanded');
// 根据列的类型添加不同的样式
if (columnId === 'iwara-column') {
column.classList.add('iwara-expanded');
} else if (columnId === 'hanime1me-column') {
column.classList.add('hanime-expanded');
} else if (columnId === 'matched-column') {
column.classList.add('matched-expanded');
}
// 更新所有列的位置
updateColumnPositions();
}
}
// --- 更新列位置 ---
function updateColumnPositions() {
const container = document.getElementById('compare-container');
const allColumns = Array.from(container.querySelectorAll('.compare-column'));
const visibleColumns = allColumns.filter(col => col.style.display !== 'none');
const columnCount = visibleColumns.length;
// 获取容器实际宽度
const containerWidth = container.offsetWidth;
// 总间距 = (列数 - 1) * 10px
const totalGap = (columnCount - 1) * 10;
// 每列的实际宽度(像素)
const columnPixelWidth = (containerWidth - totalGap) / columnCount;
// 列宽度百分比
const columnWidthPercent = (columnPixelWidth / containerWidth) * 100;
// 更新所有占位符的宽度(确保响应式)
visibleColumns.forEach((col, index) => {
const placeholderId = col.id + '-placeholder';
const placeholder = document.getElementById(placeholderId);
if (placeholder) {
placeholder.style.width = columnPixelWidth + 'px';
placeholder.style.minWidth = columnPixelWidth + 'px';
placeholder.style.maxWidth = columnPixelWidth + 'px';
}
});
// 重置所有列的定位样式
visibleColumns.forEach(col => {
if (!col.classList.contains('expanded')) {
col.style.position = '';
col.style.left = '';
col.style.right = '';
col.style.width = '';
col.style.top = '';
col.style.height = '';
col.style.zIndex = '';
}
});
// 为扩展的列设置定位
visibleColumns.forEach(col => {
if (col.classList.contains('expanded')) {
const colIndex = visibleColumns.indexOf(col);
col.style.position = 'absolute';
col.style.top = '45px';
col.style.height = 'calc(100% - 57px)';
col.style.zIndex = '10';
col.style.background = 'rgba(0, 0, 0, 0.95)';
col.style.boxShadow = '0 0 30px rgba(0, 204, 255, 0.3)';
// 根据列的类型设置不同的颜色
if (col.id === 'iwara-column') {
col.style.border = '2px solid #ffffffff';
} else if (col.id === 'hanime1me-column') {
col.style.border = '2px solid #ff0000ff';
} else if (col.id === 'matched-column') {
col.style.border = '2px solid #00ff88';
}
// 计算左侧位置:列索引 * (列宽 + 间距)
const leftPercent = colIndex * (columnWidthPercent + (10 / containerWidth) * 100);
col.style.left = leftPercent + '%';
col.style.width = columnWidthPercent + '%';
}
});
}
// --- 居中面板 (优化版) ---
function centerPanel() {
if (!panel) return;
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
// 获取面板尺寸
const panelWidth = panel.offsetWidth || 700;
const panelHeight = panel.offsetHeight || 600;
// 计算居中位置
let left = (windowWidth - panelWidth) / 2;
let top = (windowHeight - panelHeight) / 2;
// 确保面板不会超出屏幕边界
left = Math.max(10, Math.min(left, windowWidth - panelWidth - 10));
top = Math.max(10, Math.min(top, windowHeight - panelHeight - 10));
// 应用位置
panel.style.left = left + 'px';
panel.style.top = top + 'px';
panel.style.right = 'auto';
panel.style.bottom = 'auto';
// 确保面板在可视区域内
if (panelHeight > windowHeight - 20) {
panel.style.height = (windowHeight - 20) + 'px';
panel.style.top = '10px';
}
}
// --- 初始化面板 ---
function initPanel() {
panel = document.createElement('div');
panel.id = 'matcher-panel';
const savedThreshold = GM_getValue('match_threshold', 0.5);
const savedTolerance = GM_getValue('duration_tolerance', 10);
panel.innerHTML = `
标题匹配阈值:
${savedThreshold}
`;
document.body.appendChild(panel);
// 初始化元素引用
progressBar = document.getElementById('progress-bar');
progressContainer = document.getElementById('p-container');
thresholdInput = document.getElementById('threshold-range');
thresholdVal = document.getElementById('threshold-val');
toleranceInput = document.getElementById('tolerance');
// 交换列模式变量
let swapMode = false;
let selectedColumn = null;
// 绑定事件
document.getElementById('btn-cap').onclick = capturePage;
document.getElementById('btn-comp').onclick = performCompare;
document.getElementById('btn-clr').onclick = clearPools;
document.getElementById('btn-toggle-matched').onclick = () => {
const matchedColumn = document.getElementById('matched-column');
const btn = document.getElementById('btn-toggle-matched');
if (matchedColumn.style.display === 'none' || matchedColumn.style.display === '') {
matchedColumn.style.display = 'flex';
btn.innerText = '📋 隐藏成功列';
// 如果在交换模式下,为新显示的匹配列添加蓝色边框
if (swapMode) {
matchedColumn.classList.add('column-swap-ready');
}
} else {
matchedColumn.style.display = 'none';
btn.innerText = '📋 显示匹配成功列';
// 如果在交换模式下且匹配列被选中,清除选中状态
if (selectedColumn && selectedColumn.id === 'matched-column') {
selectedColumn.classList.remove('column-swap-selected');
selectedColumn = null;
}
}
centerPanel();
// 更新列位置
setTimeout(updateColumnPositions, 100);
};
// 交换列模式按钮
const swapModeBtn = document.getElementById('btn-swap-mode');
swapModeBtn.onclick = () => {
swapMode = !swapMode;
swapModeBtn.classList.toggle('active', swapMode);
swapModeBtn.innerText = swapMode ? '✅ 点击列交换' : '🔄 交换列';
// 进入交换模式时,自动收缩所有展开的列
if (swapMode) {
const columns = panel.querySelectorAll('.compare-column');
columns.forEach(col => {
if (col.classList.contains('expanded')) {
// 获取列对应的列表 ID
let listId;
if (col.id === 'iwara-column') {
listId = 'iwara-only';
} else if (col.id === 'hanime1me-column') {
listId = 'hanime1me-only';
} else if (col.id === 'matched-column') {
listId = 'matched-list';
}
// 调用收缩逻辑
if (listId) {
toggleColumnExpand(col.id, listId);
}
}
});
}
const columns = panel.querySelectorAll('.compare-column');
columns.forEach(col => {
if (col.style.display !== 'none') {
if (swapMode) {
col.classList.add('column-swap-ready');
} else {
col.classList.remove('column-swap-ready', 'column-swap-selected');
}
}
});
// 退出交换模式时清除选中状态
if (!swapMode && selectedColumn) {
selectedColumn.classList.remove('column-swap-selected');
selectedColumn = null;
}
};
document.getElementById('close-panel').onclick = () => {
panel.style.display = 'none';
toggleBtn.innerHTML = '⚡';
toggleBtn.title = '打开比对面板';
};
// 列标题点击事件 - 动态扩展
document.getElementById('iwara-header').onclick = (e) => {
if (swapMode) {
e.stopPropagation();
handleColumnSwap('iwara-column');
} else {
toggleColumnExpand('iwara-column', 'iwara-only');
}
};
document.getElementById('hanime1me-header').onclick = (e) => {
if (swapMode) {
e.stopPropagation();
handleColumnSwap('hanime1me-column');
} else {
toggleColumnExpand('hanime1me-column', 'hanime1me-only');
}
};
document.getElementById('matched-header').onclick = (e) => {
if (swapMode) {
e.stopPropagation();
handleColumnSwap('matched-column');
} else {
toggleColumnExpand('matched-column', 'matched-list');
}
};
// 处理列交换
function handleColumnSwap(columnId) {
const clickedColumn = document.getElementById(columnId);
if (!clickedColumn || clickedColumn.style.display === 'none') return;
if (!selectedColumn) {
// 第一次点击,选中该列
selectedColumn = clickedColumn;
clickedColumn.classList.add('column-swap-selected');
// 保持蓝色边框,不移除 column-swap-ready
} else {
// 第二次点击,交换两列
if (selectedColumn.id !== columnId) {
// 交换两列的位置
const container = document.getElementById('compare-container');
const placeholder = document.createElement('div');
placeholder.style.display = 'none';
// 使用占位符交换位置
container.insertBefore(placeholder, selectedColumn);
container.insertBefore(selectedColumn, clickedColumn);
container.insertBefore(clickedColumn, placeholder);
container.removeChild(placeholder);
// 更新列的 data-column 属性
const col1 = selectedColumn;
const col2 = clickedColumn;
const tempData = col1.getAttribute('data-column');
col1.setAttribute('data-column', col2.getAttribute('data-column'));
col2.setAttribute('data-column', tempData);
// 清除选中状态,但保持蓝色边框
selectedColumn.classList.remove('column-swap-selected');
clickedColumn.classList.remove('column-swap-selected');
selectedColumn = null;
// 重新居中面板
centerPanel();
} else {
// 点击同一列,取消选择
selectedColumn.classList.remove('column-swap-selected');
selectedColumn = null;
}
}
}
// 阈值事件
thresholdInput.oninput = (e) => {
thresholdVal.innerText = parseFloat(e.target.value).toFixed(2);
GM_setValue('match_threshold', e.target.value);
};
// 容差事件
toleranceInput.onchange = (e) => {
GM_setValue('duration_tolerance', parseInt(e.target.value) || 30);
};
// 面板大小调整时自动居中
const resizeObserver = new ResizeObserver(() => {
if (panel && panel.style.display === 'flex') {
centerPanel();
// 同时更新列位置(包括占位符宽度)
updateColumnPositions();
}
});
resizeObserver.observe(panel);
// 监听窗口大小变化
window.addEventListener('resize', () => {
if (panel && panel.style.display === 'flex') {
updateColumnPositions();
}
});
}
// --- 捕获 iwara.tv 页面数据 ---
function captureiwara() {
const videos = [];
const teasers = document.querySelectorAll('.videoTeaser');
teasers.forEach(el => {
try {
const titleEl = el.querySelector('.videoTeaser__title');
const title = titleEl?.innerText.trim() || '';
const linkEl = el.querySelector('.videoTeaser__thumbnail');
const link = linkEl?.getAttribute('href');
const url = link ? 'https://www.iwara.tv' + link : '';
const durationEl = el.querySelector('.duration .text');
const duration = durationEl?.innerText.trim() || '';
if (title && url) {
videos.push({
title,
url,
duration,
seconds: parseDuration(duration)
});
}
} catch (e) {
console.error('Error parsing iwara video:', e);
}
});
return videos;
}
// --- 捕获 hanime1.me 页面数据 ---
function capturehanime1() {
const videos = [];
const containers = document.querySelectorAll('.video-item-container');
containers.forEach(el => {
try {
const title = el.getAttribute('title') ||
el.querySelector('.title')?.innerText.trim() ||
'';
const linkEl = el.querySelector('.video-link');
const url = linkEl?.getAttribute('href') || '';
const durationEl = el.querySelector('.duration');
const duration = durationEl?.innerText.trim() || '';
if (title && url) {
videos.push({
title,
url,
duration,
seconds: parseDuration(duration)
});
}
} catch (e) {
console.error('Error parsing hanime1 video:', e);
}
});
return videos;
}
// --- 捕获页面数据 ---
function capturePage() {
const host = window.location.host;
let poolKey, videos;
if (host.includes('iwara.tv')) {
poolKey = 'iwara_pool';
videos = captureiwara();
} else if (host.includes('hanime')) {
// 所有 Hanime 网站共享同一个数据池
poolKey = 'hanime1me_pool';
videos = capturehanime1();
} else {
return;
}
let pool = GM_getValue(poolKey, []) || [];
let added = 0;
let skipped = 0;
videos.forEach(v => {
// 只保存包含时长信息的视频
if (v.seconds === null || v.seconds === 0) {
skipped++;
return;
}
if (!pool.some(p => p.url === v.url)) {
pool.push(v);
added++;
}
});
GM_setValue(poolKey, pool);
updateStats();
alert(`新增 ${added} 条,总计 ${pool.length}\n本次捕获:${videos.length} 条\n已保存:${added} 条 (有时长)\n已舍弃:${skipped} 条 (无时长)`);
}
// --- 核心比对业务 ---
async function performCompare() {
const iwara = GM_getValue('iwara_pool', []) || [];
const hanime1me = GM_getValue('hanime1me_pool', []) || [];
const thres = parseFloat(thresholdInput.value);
const tolerance = parseInt(toleranceInput.value) || 30;
const iwaraWithDuration = iwara.filter(v => v.seconds !== null);
const hanime1meWithDuration = hanime1me.filter(v => v.seconds !== null);
const iOnlyDiv = document.getElementById('iwara-only');
const hOnlyDiv = document.getElementById('hanime1me-only');
const matchedListDiv = document.getElementById('matched-list');
iOnlyDiv.innerHTML = hOnlyDiv.innerHTML = matchedListDiv.innerHTML = '比对中...
';
progressContainer.style.display = 'block';
progressBar.style.width = '0%';
let iOnly = [];
let hOnly = [];
let matchedPairs = [];
// 计算 iwara 独有,并记录最高相似度
for (let i = 0; i < iwaraWithDuration.length; i++) {
const iItem = iwaraWithDuration[i];
let maxSimilarity = 0;
let bestMatch = null;
for (const hItem of hanime1meWithDuration) {
if (!isDurationMatch(iItem.seconds, hItem.seconds, tolerance)) {
continue;
}
const titleSim = titleSimilarity(
cleanTitle(iItem.title),
cleanTitle(hItem.title)
);
if (titleSim > maxSimilarity) {
maxSimilarity = titleSim;
bestMatch = hItem;
}
}
// 只添加低于阈值的项
if (maxSimilarity < thres) {
iOnly.push({
...iItem,
maxSimilarity
});
} else if (bestMatch) {
// 记录匹配成功的项
matchedPairs.push({
iwara: iItem,
hanime1me: bestMatch,
similarity: maxSimilarity
});
}
if (i % 5 === 0) {
const percent = (i / iwaraWithDuration.length) * 50;
progressBar.style.width = percent + '%';
await new Promise(r => setTimeout(r, 0));
}
}
// 计算 hanime1 独有,并记录最高相似度
for (let i = 0; i < hanime1meWithDuration.length; i++) {
const hItem = hanime1meWithDuration[i];
let maxSimilarity = 0;
for (const iItem of iwaraWithDuration) {
if (!isDurationMatch(hItem.seconds, iItem.seconds, tolerance)) {
continue;
}
const titleSim = titleSimilarity(
cleanTitle(hItem.title),
cleanTitle(iItem.title)
);
maxSimilarity = Math.max(maxSimilarity, titleSim);
}
// 只添加低于阈值的项
if (maxSimilarity < thres) {
hOnly.push({
...hItem,
maxSimilarity
});
}
if (i % 5 === 0) {
const percent = 50 + (i / hanime1meWithDuration.length) * 50;
progressBar.style.width = percent + '%';
await new Promise(r => setTimeout(r, 0));
}
}
progressBar.style.width = '100%';
setTimeout(() => progressContainer.style.display = 'none', 500);
renderResults(iOnly, hOnly, matchedPairs, iwaraWithDuration, hanime1meWithDuration, thres, tolerance);
}
// --- 渲染结果 (添加最高相似度显示) ---
function renderResults(iOnly, hOnly, matchedPairs, iwaraWithDuration, hanime1meWithDuration, thres, tolerance) {
const iOnlyDiv = document.getElementById('iwara-only');
const hOnlyDiv = document.getElementById('hanime1me-only');
const matchedListDiv = document.getElementById('matched-list');
iOnlyDiv.innerHTML = '';
hOnlyDiv.innerHTML = '';
matchedListDiv.innerHTML = '';
// 按相似度降序排列匹配成功的项
matchedPairs.sort((a, b) => b.similarity - a.similarity);
// 渲染 iwara 独有
iOnly.forEach(item => {
const div = document.createElement('div');
div.className = 'diff-item';
// 计算相似项(最多 3 个)
const similarities = hanime1meWithDuration
.map(op => {
const titleSim = titleSimilarity(
cleanTitle(item.title),
cleanTitle(op.title)
);
return {
title: op.title,
url: op.url,
duration: op.duration,
seconds: op.seconds,
titleSim,
durationDiff: Math.abs(item.seconds - op.seconds)
};
})
.filter(s => s.titleSim >= thres * 0.6)
.sort((a, b) => b.titleSim - a.titleSim)
.slice(0, 3);
// 只有有相似项才显示相似度徽章
const hasSimilar = similarities.length > 0;
const maxSim = hasSimilar ? similarities[0].titleSim : 0;
const titleHtml = `
${item.title}
${hasSimilar ?
`
${(maxSim * 100).toFixed(0)}%
` : ''
}
${formatDuration(item.seconds)}
`;
div.innerHTML = titleHtml;
// 将相似项附加到元素上供悬浮提示使用
div._similarities = similarities;
addHoverSimilarity(div, item, hanime1meWithDuration, thres, tolerance, 'iwara');
iOnlyDiv.appendChild(div);
});
// 渲染 hanime1 独有
hOnly.forEach(item => {
const div = document.createElement('div');
div.className = 'diff-item';
// 计算相似项(最多 3 个)
const similarities = iwaraWithDuration
.map(op => {
const titleSim = titleSimilarity(
cleanTitle(item.title),
cleanTitle(op.title)
);
return {
title: op.title,
url: op.url,
duration: op.duration,
seconds: op.seconds,
titleSim,
durationDiff: Math.abs(item.seconds - op.seconds)
};
})
.filter(s => s.titleSim >= thres * 0.6)
.sort((a, b) => b.titleSim - a.titleSim)
.slice(0, 3);
// 只有有相似项才显示相似度徽章
const hasSimilar = similarities.length > 0;
const maxSim = hasSimilar ? similarities[0].titleSim : 0;
const titleHtml = `
${item.title}
${hasSimilar ?
`
${(maxSim * 100).toFixed(0)}%
` : ''
}
${formatDuration(item.seconds)}
`;
div.innerHTML = titleHtml;
// 将相似项附加到元素上供悬浮提示使用
div._similarities = similarities;
addHoverSimilarity(div, item, iwaraWithDuration, thres, tolerance, 'hanime1');
hOnlyDiv.appendChild(div);
});
// 渲染匹配成功的项
matchedPairs.forEach(pair => {
const div = document.createElement('div');
div.className = 'diff-item';
div.style.background = '#1a2a2a';
const titleHtml = `
${(pair.similarity * 100).toFixed(1)}%
`;
div.innerHTML = titleHtml;
// 为匹配成功的项添加悬浮提示,显示其他可能的匹配项
addHoverSimilarityForMatched(div, pair, iwaraWithDuration, hanime1meWithDuration, thres, tolerance);
matchedListDiv.appendChild(div);
});
document.getElementById('diff-i').innerText = iOnly.length;
document.getElementById('diff-h').innerText = hOnly.length;
document.getElementById('diff-matched').innerText = matchedPairs.length;
// 显示过滤统计
const iFiltered = iwaraWithDuration.length;
const hFiltered = hanime1meWithDuration.length;
const iTotal = GM_getValue('iwara_pool', []).length;
const hTotal = GM_getValue('hanime1me_pool', []).length;
if (iFiltered < iTotal || hFiltered < hTotal) {
const filterNotice = document.createElement('div');
filterNotice.style.cssText = 'text-align: center; font-size: 10px; color: #ffaa00; margin-top: 5px;';
filterNotice.innerHTML = `⏱️ 已过滤无时长视频: iwara ${iTotal - iFiltered}条, hanime1 ${hTotal - hFiltered}条`;
panel.querySelector('.compare-container').before(filterNotice);
setTimeout(() => filterNotice.remove(), 5000);
}
if (iOnly.length === 0) {
iOnlyDiv.innerHTML = '没有独有项
';
}
if (hOnly.length === 0) {
hOnlyDiv.innerHTML = '没有独有项
';
}
}
// --- 添加悬浮提示 ---
function addHoverSimilarity(div, item, oppositePool, thres, tolerance, source) {
// 使用预先计算并附加在元素上的相似项
const similarities = div._similarities || [];
if (similarities.length === 0) return;
div.addEventListener('mouseenter', (e) => {
const tooltip = document.createElement('div');
tooltip.id = 'similarity-tooltip';
tooltip.innerHTML = `
最相似的${similarities.length}项:
${similarities.map(s => `
${s.title}
${(s.titleSim * 100).toFixed(1)}%
⏱️ ${s.duration} | 相差 ${s.durationDiff}秒
`).join('')}
`;
tooltip.style.cssText = `
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;
`;
document.body.appendChild(tooltip);
const rect = div.getBoundingClientRect();
let left = source === 'iwara' ? rect.right + 10 : rect.left - 260;
let top = rect.top;
if (source === 'iwara' && rect.right + 260 > window.innerWidth) {
left = rect.left - 260;
} else if (source === 'hanime1' && rect.left - 260 < 0) {
left = rect.right + 10;
}
if (rect.top + tooltip.offsetHeight + 10 > window.innerHeight) {
top = window.innerHeight - tooltip.offsetHeight - 10;
}
tooltip.style.left = Math.max(10, left) + 'px';
tooltip.style.top = Math.max(10, Math.min(top, window.innerHeight - tooltip.offsetHeight - 10)) + 'px';
});
div.addEventListener('mouseleave', () => {
const tooltip = document.getElementById('similarity-tooltip');
if (tooltip) tooltip.remove();
});
}
// --- 为匹配成功的项添加悬浮提示 ---
function addHoverSimilarityForMatched(div, pair, iwaraPool, hanimePool, thres, tolerance) {
// 查找 iwara 的其他相似项
const otherIwaraSimilar = hanimePool
.filter(op => op.url !== pair.hanime1me.url)
.map(op => {
const titleSim = titleSimilarity(
cleanTitle(pair.iwara.title),
cleanTitle(op.title)
);
return {
title: op.title,
url: op.url,
duration: op.duration,
seconds: op.seconds,
titleSim,
durationDiff: Math.abs(pair.iwara.seconds - op.seconds),
type: 'hanime'
};
})
.filter(s => s.titleSim >= thres * 0.6)
.sort((a, b) => b.titleSim - a.titleSim)
.slice(0, 2);
// 查找 hanime 的其他相似项
const otherHanimeSimilar = iwaraPool
.filter(op => op.url !== pair.iwara.url)
.map(op => {
const titleSim = titleSimilarity(
cleanTitle(pair.hanime1me.title),
cleanTitle(op.title)
);
return {
title: op.title,
url: op.url,
duration: op.duration,
seconds: op.seconds,
titleSim,
durationDiff: Math.abs(pair.hanime1me.seconds - op.seconds),
type: 'iwara'
};
})
.filter(s => s.titleSim >= thres * 0.6)
.sort((a, b) => b.titleSim - a.titleSim)
.slice(0, 2);
const allSimilar = [...otherIwaraSimilar, ...otherHanimeSimilar];
if (allSimilar.length === 0) return;
div.addEventListener('mouseenter', (e) => {
const tooltip = document.createElement('div');
tooltip.id = 'similarity-tooltip';
tooltip.innerHTML = `
其他可能的匹配项:
${allSimilar.map(s => `
${s.title}
${(s.titleSim * 100).toFixed(1)}%
⏱️ ${s.duration} | 相差 ${s.durationDiff}秒
`).join('')}
`;
tooltip.style.cssText = `
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;
`;
document.body.appendChild(tooltip);
const rect = div.getBoundingClientRect();
let left = rect.right + 10;
let top = rect.top;
if (rect.right + 260 > window.innerWidth) {
left = rect.left - 260;
}
if (rect.top + tooltip.offsetHeight + 10 > window.innerHeight) {
top = window.innerHeight - tooltip.offsetHeight - 10;
}
tooltip.style.left = Math.max(10, left) + 'px';
tooltip.style.top = Math.max(10, Math.min(top, window.innerHeight - tooltip.offsetHeight - 10)) + 'px';
});
div.addEventListener('mouseleave', () => {
const tooltip = document.getElementById('similarity-tooltip');
if (tooltip) tooltip.remove();
});
}
// --- 清空数据 ---
function clearPools() {
if (confirm('清空所有存储的数据?')) {
GM_setValue('iwara_pool', []);
GM_setValue('hanime1me_pool', []);
updateStats();
if (panel && panel.style.display === 'flex') {
performCompare();
}
}
}
// --- 更新统计 ---
function updateStats() {
const iwaraPool = GM_getValue('iwara_pool', []) || [];
const hanime1mePool = GM_getValue('hanime1me_pool', []) || [];
document.getElementById('total-i').innerText = iwaraPool.length;
document.getElementById('total-h').innerText = hanime1mePool.length;
}
// --- 初始化 ---
function init() {
console.log('[iwara & hanime1me] 初始化插件...');
createToggleButton();
}
// 立即初始化插件
console.log('[iwara & hanime1me] 脚本执行,立即初始化');
init();
})();