// ==UserScript==
// @name 好き嫌い検索
// @namespace http://tampermonkey.net/
// @version 1.2
// @description 好き嫌い検索
// @match https://suki-kira.com/people/result/*
// @grant none
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
////////////////////////////////////////////
// 1. UI生成
////////////////////////////////////////////
const ui = document.createElement('div');
Object.assign(ui.style, {
position: 'fixed', top: '10px', right: '10px',
padding: '8px', background: 'rgba(255,255,255,0.95)',
border: '1px solid #ccc', zIndex: '9999'
});
ui.innerHTML = `
`;
document.body.appendChild(ui);
const logEl = ui.querySelector('#skiLog');
function log(msg) {
console.log('[SkiTool] ' + msg);
const d = document.createElement('div');
d.textContent = msg;
logEl.appendChild(d);
logEl.scrollTop = logEl.scrollHeight;
}
////////////////////////////////////////////
// 2. 検索キーワード解析 (AND/ORグループ生成)
////////////////////////////////////////////
function parseSearchGroups(input) {
const groups = [];
// 1) " " で囲まれた部分を ORグループとして抜き出し
const quoteRe = /"([^"]+)"/g;
let m;
const used = [];
while ((m = quoteRe.exec(input))) {
const orTerms = m[1]
.split(/\s+/)
.map(s => s.trim())
.filter(Boolean);
if (orTerms.length) {
groups.push(orTerms);
used.push(m[0]);
}
}
// 2) 残りの文字列から単語を抜き、各々AND用グループ(1ワード OR)
let remainder = input;
used.forEach(tok => {
remainder = remainder.replace(tok, ' ');
});
remainder
.split(/\s+/)
.map(s => s.trim())
.filter(Boolean)
.forEach(word => {
groups.push([word]);
});
return groups;
}
////////////////////////////////////////////
// 3. START ボタン押したら
////////////////////////////////////////////
ui.querySelector('#skiStartBtn').addEventListener('click', () => {
const raw = ui.querySelector('#skiKeyword').value.trim();
const startId = parseInt(ui.querySelector('#skiStart').value, 10);
const endId = parseInt(ui.querySelector('#skiEnd').value, 10);
if (!raw || isNaN(startId) || isNaN(endId) || startId <= endId) {
alert('キーワード/開始/終了 を正しく指定してね');
return;
}
const groups = parseSearchGroups(raw);
log(`検索条件: ${JSON.stringify(groups)}`);
ui.querySelector('#skiStartBtn').disabled = true;
runScan(groups, startId, endId)
.catch(err => {
console.error(err);
log('エラー: ' + err.message);
ui.querySelector('#skiStartBtn').disabled = false;
});
});
////////////////////////////////////////////
// 4. レスの走査ループ
////////////////////////////////////////////
async function runScan(groups, startId, endId) {
const baseUrl = location.origin + location.pathname;
let currentNxc = startId + 1;
const seen = new Set();
const results = [];
while (true) {
const url = `${baseUrl}?nxc=${currentNxc}`;
log(`→ ページ取得 ${url}`);
const html = await fetchPage(url);
const doc = new DOMParser().parseFromString(html, 'text/html');
const comments = Array.from(doc.querySelectorAll('.comment-container'));
if (!comments.length) {
log('警告: レス0件');
break;
}
let minId = Infinity;
for (const c of comments) {
const id = parseInt(c.id, 10);
if (isNaN(id)) continue;
minId = Math.min(minId, id);
if (id > startId || id < endId || seen.has(id)) continue;
// 本文テキスト取得、小文字化
const bodyEl = c.querySelector('.comment_body');
const text = bodyEl
? bodyEl.textContent.replace(/\u00A0/g,' ')
: '';
const low = text.toLowerCase();
// AND/ORグループ一致判定
const ok = groups.every(group => {
return group.some(term => {
return low.includes(term.toLowerCase());
});
});
if (ok) {
seen.add(id);
results.push({ id, text });
log(` Hit: ${id}`);
}
}
if (minId <= endId) {
log(`終了: ${minId} ≤ ${endId}`);
break;
}
currentNxc = minId;
await sleep(500);
}
makeDownload(results);
ui.querySelector('#skiStartBtn').disabled = false;
}
////////////////////////////////////////////
// 5. ページ取得
////////////////////////////////////////////
async function fetchPage(url) {
const res = await fetch(url, {
method: 'GET',
credentials: 'include',
headers: { 'Referer': location.href }
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.text();
}
function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
////////////////////////////////////////////
// 6. テキスト出力
////////////////////////////////////////////
function makeDownload(items) {
if (!items.length) {
log('該当レスがないよ');
}
let txt = '';
items.sort((a,b) => b.id - a.id);
for (const {id, text} of items) {
txt += `${id}:\n${text}\n\n`;
}
const blob = new Blob([txt], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `sukikira-${Date.now()}.txt`;
a.textContent = `結果をダウンロード (${items.length} 件)`;
a.style.display = 'block';
ui.querySelector('#skiDownload').appendChild(a);
log('ファイル準備完了');
}
})();