Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name Easychan/Mokachan Archive
- // @namespace http://tampermonkey.net/
- // @version 1.0
- // @match https://easychan.net/*
- // @match https://mokachan.cafe/*
- // @grant none
- // ==/UserScript==
- (function () {
- 'use strict';
- const API_URL = 'https://meguca-archive.onrender.com';
- const SHA1_RE = /\/([0-9a-f]{40})\.\w/;
- const SOURCE = location.hostname.includes('mokachan') ? 'mokachan' : 'easychan';
- const ORIGIN_BASES = { easychan: 'https://easychan.net', mokachan: 'https://mokachan.cafe' };
- // -----------------------------------------------------------------------
- // SERVER API
- // -----------------------------------------------------------------------
- async function searchPostsAdvanced(params) {
- const qs = new URLSearchParams();
- if (params.text) qs.set('q', params.text);
- if (params.sha1) qs.set('sha1', params.sha1);
- if (params.filename) qs.set('filename', params.filename);
- if (params.post) qs.set('post_id', params.post);
- if (params.thread) qs.set('thread_id', params.thread);
- if (params.board) qs.set('board', params.board);
- if (params.dateStart) qs.set('date_start', params.dateStart);
- if (params.dateEnd) qs.set('date_end', params.dateEnd);
- qs.set('source', params.source || SOURCE);
- qs.set('limit', '1000');
- const res = await fetch(`${API_URL}/search?${qs.toString()}`);
- if (!res.ok) throw new Error(`Search failed: ${res.status}`);
- return (await res.json()).posts ?? [];
- }
- // -----------------------------------------------------------------------
- // "FIND ALL POSTS" CONTEXT MENU ITEM
- // -----------------------------------------------------------------------
- const menuObserver = new MutationObserver((mutations) => {
- for (const mutation of mutations) {
- for (const node of mutation.addedNodes) {
- if (!(node instanceof Element)) continue;
- const menu = node.matches('ul.popup-menu') ? node : node.querySelector('ul.popup-menu');
- if (!menu) continue;
- const fileNamesItem = menu.querySelector('[data-id="viewFileNames"]');
- if (!fileNamesItem) continue;
- if (menu.querySelector('[data-id="globalHashSearch"]')) continue;
- const li = document.createElement('li');
- li.setAttribute('data-id', 'globalHashSearch');
- li.textContent = 'Find All Posts';
- menu.insertBefore(li, fileNamesItem.nextSibling);
- }
- }
- });
- menuObserver.observe(document.body, { childList: true, subtree: true });
- document.addEventListener('click', async (e) => {
- const target = e.target;
- if (!(target instanceof Element)) return;
- if (target.getAttribute('data-id') !== 'globalHashSearch') return;
- const article = target.closest('article');
- if (!article) return;
- const sha1 = getSha1FromDOM(article);
- if (!sha1) { alert('Could not find image hash for this post.'); return; }
- target.closest('ul.popup-menu')?.remove();
- try {
- const results = await searchPostsAdvanced({ sha1, source: SOURCE });
- if (!results.length) { alert('No posts found with this image hash.'); return; }
- showModal(results, `${results.length} post${results.length !== 1 ? 's' : ''} with this image`);
- } catch (e) {
- alert(`Search failed: ${e.message}`);
- }
- }, true);
- // -----------------------------------------------------------------------
- // MODAL DISPLAY
- // -----------------------------------------------------------------------
- function showModal(posts, title) {
- const overlay = document.getElementById('modal-overlay');
- if (!overlay) return;
- overlay.querySelectorAll('.post-collection').forEach(el => el.remove());
- const modal = document.createElement('div');
- modal.className = 'modal post-collection';
- modal.style.display = 'block';
- const closer = document.createElement('a');
- closer.textContent = '[X]';
- closer.style.cssFloat = 'right';
- closer.style.cursor = 'pointer';
- closer.addEventListener('click', () => {
- modal.remove();
- openSearchPanel();
- }, { passive: true });
- modal.append(closer);
- if (title) {
- const titleDiv = document.createElement('div');
- titleDiv.style.margin = '0 0 10px 0';
- const h3 = document.createElement('h3');
- h3.textContent = title;
- h3.style.cssText = 'margin:0; padding:5px 0; display:inline;';
- titleDiv.append(h3);
- modal.append(titleDiv);
- }
- const PAGE_SIZE = 50;
- let currentPage = 0;
- const sorted = [...posts].sort((a, b) => (b.time || b.id) - (a.time || a.id));
- const postsContainer = document.createElement('div');
- modal.append(postsContainer);
- const pageInfo = document.createElement('div');
- pageInfo.style.cssText = 'display:flex;align-items:center;gap:8px;margin:10px 0 4px;font-size:12px;opacity:0.7;border-top:1px solid currentColor;padding-top:8px;';
- const prevBtn = document.createElement('a');
- prevBtn.textContent = '← Prev';
- prevBtn.style.cssText = 'cursor:pointer;';
- const nextBtn = document.createElement('a');
- nextBtn.textContent = 'Next →';
- nextBtn.style.cssText = 'cursor:pointer;';
- const pageLabel = document.createElement('span');
- pageLabel.style.cssText = 'flex:1; text-align:center;';
- const limitNote = document.createElement('span');
- limitNote.textContent = 'Only first 1000 results shown';
- limitNote.style.cssText = 'font-size:11px; opacity:0.5; display:block; text-align:center; margin-top:4px;';
- pageInfo.append(prevBtn, pageLabel, nextBtn);
- modal.append(pageInfo, limitNote);
- function renderPage(page) {
- currentPage = page;
- const totalPages = Math.ceil(sorted.length / PAGE_SIZE);
- const start = page * PAGE_SIZE;
- const slice = sorted.slice(start, start + PAGE_SIZE);
- postsContainer.innerHTML = '';
- for (const p of slice) postsContainer.append(buildPostStub(p));
- pageLabel.textContent = `Page ${page + 1} of ${totalPages} (${sorted.length} results)`;
- prevBtn.style.opacity = page === 0 ? '0.3' : '1';
- prevBtn.style.pointerEvents = page === 0 ? 'none' : 'auto';
- nextBtn.style.opacity = page >= totalPages - 1 ? '0.3' : '1';
- nextBtn.style.pointerEvents = page >= totalPages - 1 ? 'none' : 'auto';
- modal.scrollTop = 0;
- }
- prevBtn.addEventListener('click', () => renderPage(currentPage - 1));
- nextBtn.addEventListener('click', () => renderPage(currentPage + 1));
- renderPage(0);
- overlay.append(modal);
- }
- function buildPostStub(p) {
- const date = p.time ? new Date(p.time * 1000).toLocaleString('en-GB', {
- day: '2-digit', month: 'short', year: 'numeric', weekday: 'short',
- hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
- }) : '';
- const postBase = ORIGIN_BASES[p.source] ?? ORIGIN_BASES[SOURCE];
- const isStaleThumb = p.thumb_url && (p.thumb_url.includes('supabase.co') || p.thumb_url.includes('/thumb/'));
- const thumbUrl = (!isStaleThumb && p.thumb_url)
- ? (p.thumb_url.startsWith('http') ? p.thumb_url : `${postBase}${p.thumb_url}`)
- : (p.sha1 ? `${postBase}/assets/images/thumb/${p.sha1}.webp` : null);
- const srcUrl = p.src_url
- ? (p.src_url.startsWith('http') ? p.src_url : `${postBase}${p.src_url}`)
- : (p.sha1 ? `${postBase}/assets/images/src/${p.sha1}` : null);
- const filename = p.filename ?? p.sha1 ?? '';
- const isOp = p.id === p.thread;
- const isAdmin = p.role === 'admin';
- const isMod = p.role === 'mod';
- const article = document.createElement('article');
- article.id = `archive-p${p.id}`;
- article.className = 'glass media';
- article.style.cssText = [
- 'border: 1px solid rgba(128,128,128,0.25)',
- 'padding: 8px 12px',
- 'margin-bottom: 6px',
- isAdmin ? 'border-color: rgba(200,50,50,0.6)' : '',
- isMod ? 'border-color: rgba(0,120,120,0.6)' : '',
- isOp ? 'border-left: 3px solid rgba(100,100,220,0.5)' : '',
- ].filter(Boolean).join(';');
- const threadLink = p.thread && p.board
- ? `<a href="${postBase}/${escHtml(p.board)}/${p.thread}#p${p.id}" target="_blank" style="margin-left:6px;opacity:.6;font-size:11px;">→ /${escHtml(p.board)}/ #${p.thread}</a>`
- : '';
- const badges = [
- isOp ? `<span style="background:rgba(80,80,200,0.3);color:#aaf;font-size:10px;padding:1px 5px;border-radius:2px;margin-left:5px;">OP</span>` : '',
- isAdmin ? `<span style="background:#a00;color:#fff;font-size:10px;padding:1px 5px;border-radius:2px;margin-left:5px;">## Admin</span>` : '',
- isMod ? `<span style="background:#055;color:#fff;font-size:10px;padding:1px 5px;border-radius:2px;margin-left:5px;">## Mod</span>` : '',
- ].join('');
- const banHtml = p.ban_message
- ? `<div style="color:#e44;font-size:11px;font-weight:bold;margin-top:5px;">${escHtml(p.ban_message)}</div>`
- : '';
- article.innerHTML = `
- <header class="spaced" style="margin-bottom:5px;font-size:12px;">
- <b class="name spaced"><span>Anonymous</span>${badges}</b>
- <time style="opacity:.6;">${escHtml(date)}</time>
- <nav><a class="quote" style="font-weight:bold;">${p.id}</a>${threadLink}</nav>
- </header>
- <figcaption class="spaced" style="font-size:11px;opacity:.6;margin-bottom:4px;">
- ${p.meta ? `<span class="media-metadata">${escHtml(p.meta)}</span>` : ''}
- ${srcUrl ? `<a class="filename-link" href="${escHtml(srcUrl)}" download="${escHtml(filename)}">${escHtml(filename)}</a>` : ''}
- ${srcUrl ? `<a class="download-link symbol" href="${escHtml(srcUrl)}" target="_blank" style="margin-left:4px;">⬇</a>` : ''}
- </figcaption>
- <div class="post-container">
- ${thumbUrl ? `<figure style="margin:0 8px 0 0;"><a target="_blank" href="${escHtml(srcUrl ?? thumbUrl)}">
- <img loading="lazy" draggable="false" src="${escHtml(thumbUrl)}" style="max-height:80px;max-width:80px;object-fit:cover;display:block;"></a></figure>` : ''}
- <blockquote style="margin:0;font-size:13px;white-space:pre-wrap;word-break:break-word;">${escHtml(p.body ?? '')}</blockquote>
- </div>
- ${banHtml}`;
- return article;
- }
- // -----------------------------------------------------------------------
- // SEARCH PANEL
- // -----------------------------------------------------------------------
- function openSearchPanel() {
- document.getElementById('ec-search-panel')?.remove();
- const overlay = document.getElementById('modal-overlay');
- if (!overlay) return;
- const panel = document.createElement('div');
- panel.id = 'ec-search-panel';
- panel.className = 'modal post-collection';
- panel.style.cssText = 'display:block; padding:14px; min-width:300px; max-width:360px;';
- panel.innerHTML = `
- <style>
- #ec-search-panel .ec-row { display:grid; grid-template-columns:90px 1fr; align-items:center; gap:8px; margin-bottom:8px; }
- #ec-search-panel .ec-row label { font-size:12px; opacity:0.7; text-align:right; }
- #ec-search-panel input, #ec-search-panel select { width:100%; box-sizing:border-box; padding:3px 6px; font-size:13px; background:transparent; color:inherit; border:1px solid currentColor; border-radius:3px; outline:none; }
- #ec-search-panel .ec-btns { display:flex; gap:8px; justify-content:flex-end; margin-top:10px; padding-top:8px; border-top:1px solid currentColor; opacity:0.85; }
- #ec-search-panel .ec-btns a { cursor:pointer; font-size:13px; }
- </style>
- <a id="ec-panel-close" style="float:right; cursor:pointer;">[X]</a>
- <div style="margin-bottom:10px; font-size:13px; font-weight:bold;"><a href="https://meguca-archive.onrender.com" target="_blank" style="color:inherit;">Archive Search ↗</a></div>
- <div class="ec-row"><label>Text</label><input id="bm-arc-text" type="text" placeholder='post body (add * for all results)' /></div>
- <div class="ec-row"><label>Image hash</label><input id="ec-q-sha1" type="text" placeholder="sha1 or partial" /></div>
- <div class="ec-row"><label>Filename</label><input id="ec-q-filename" type="text" placeholder="e.g. video.mp4" /></div>
- <div class="ec-row"><label>Post no.</label><input id="ec-q-post" type="text" placeholder="e.g. 15820" /></div>
- <div class="ec-row"><label>Thread no.</label><input id="ec-q-thread" type="text" placeholder="e.g. 6621" /></div>
- <div class="ec-row"><label>Date start</label><input id="ec-q-date-start" type="date" /></div>
- <div class="ec-row"><label>Date end</label><input id="ec-q-date-end" type="date" /></div>
- ${SOURCE === 'easychan' ? `
- <div class="ec-row"><label>Board</label>
- <select id="ec-q-board">
- <option value="">All boards</option>
- <option value="kr">/kr/</option>
- </select>
- </div>` : ''}
- <div class="ec-row"><label>Site</label>
- <div>
- <select id="ec-q-source">
- <option value="${SOURCE}" selected>${SOURCE}</option>
- ${SOURCE === 'mokachan' ? '<option value="easychan">easychan</option>' : '<option value="mokachan">mokachan</option>'}
- </select>
- <div style="font-size:10px;opacity:0.45;margin-top:3px;">⚠ searching another site may return broken image links</div>
- </div>
- </div>
- <div class="ec-btns">
- <a id="ec-clear-btn">[Clear]</a>
- <a id="ec-go-btn">[Search]</a>
- </div>
- `;
- overlay.append(panel);
- document.getElementById('ec-panel-close').addEventListener('click', () => panel.remove());
- document.getElementById('ec-q-text')?.focus();
- document.getElementById('ec-clear-btn').addEventListener('click', () => {
- ['ec-q-text','ec-q-sha1','ec-q-filename','ec-q-post','ec-q-thread','ec-q-date-start','ec-q-date-end'].forEach(id => {
- const el = document.getElementById(id); if (el) el.value = '';
- });
- const board = document.getElementById('ec-q-board');
- if (board) board.value = '';
- document.getElementById('ec-q-source').value = SOURCE;
- });
- const doSearch = async () => {
- const params = {
- text: document.getElementById('ec-q-text')?.value.trim(),
- sha1: document.getElementById('ec-q-sha1')?.value.trim(),
- filename: document.getElementById('ec-q-filename')?.value.trim(),
- post: document.getElementById('ec-q-post')?.value.trim(),
- thread: document.getElementById('ec-q-thread')?.value.trim(),
- dateStart: document.getElementById('ec-q-date-start')?.value,
- dateEnd: document.getElementById('ec-q-date-end')?.value,
- board: document.getElementById('ec-q-board')?.value ?? '',
- source: document.getElementById('ec-q-source')?.value ?? SOURCE,
- };
- if (!Object.values(params).some(v => v)) { alert('Fill in at least one search field.'); return; }
- panel.remove();
- try {
- const results = await searchPostsAdvanced(params);
- if (!results.length) { alert('No results found.'); return; }
- showModal(results, `${results.length} result${results.length !== 1 ? 's' : ''}`);
- } catch (e) { alert(`Search failed: ${e.message}`); }
- };
- panel.addEventListener('keydown', (e) => { if (e.key === 'Enter') doSearch(); });
- document.getElementById('ec-go-btn').addEventListener('click', doSearch);
- }
- // -----------------------------------------------------------------------
- // INJECT SEARCH BUTTON
- // -----------------------------------------------------------------------
- function injectSearchUI() {
- if (document.getElementById('ec-search-bar')) return;
- const bar = document.createElement('span');
- bar.id = 'ec-search-bar';
- bar.style.cssText = 'display:inline-flex; align-items:center;';
- const searchBtn = document.createElement('a');
- searchBtn.className = 'banner-float svg-link noscript-hide';
- searchBtn.title = 'Search Archive';
- searchBtn.style.cursor = 'pointer';
- searchBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8" viewBox="0 0 8 8">
- <path d="M3.5 0C1.57 0 0 1.57 0 3.5c0 1.93 1.57 3.5 3.5 3.5 0.77 0 1.48-0.25 2.06-0.66L7.29 8 8 7.29 6.34 5.56C6.75 4.98 7 4.27 7 3.5 7 1.57 5.43 0 3.5 0zm0 1C4.88 1 6 2.12 6 3.5S4.88 6 3.5 6 1 4.88 1 3.5 2.12 1 3.5 1z"/>
- </svg>`;
- searchBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- if (document.getElementById('ec-search-panel')) {
- document.getElementById('ec-search-panel').remove();
- } else {
- openSearchPanel();
- }
- });
- bar.append(searchBtn);
- const inject = () => {
- const nekotv = document.getElementById('banner-nekotv');
- if (nekotv && !document.getElementById('ec-search-bar')) { nekotv.before(bar); return true; }
- const nav = document.getElementById('board-navigation');
- if (nav && !document.getElementById('ec-search-bar')) { nav.append(bar); return true; }
- return false;
- };
- const obs = new MutationObserver(() => inject());
- obs.observe(document.body, { childList: true, subtree: true });
- inject();
- }
- // -----------------------------------------------------------------------
- // HELPERS
- // -----------------------------------------------------------------------
- function getSha1FromDOM(article) {
- const img = article.querySelector('figure img');
- if (img) { const m = img.getAttribute('src')?.match(SHA1_RE); if (m) return m[1]; }
- const link = article.querySelector('a[href*="/assets/images/"]');
- if (link) { const m = link.getAttribute('href')?.match(SHA1_RE); if (m) return m[1]; }
- const dlLink = article.querySelector('a.filename-link');
- if (dlLink) { const m = dlLink.getAttribute('onclick')?.match(/sha1=([a-f0-9]{40})/); if (m) return m[1]; }
- return null;
- }
- function escHtml(str) {
- return String(str ?? '')
- .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
- }
- // -----------------------------------------------------------------------
- // INIT
- // -----------------------------------------------------------------------
- injectSearchUI();
- })();
Advertisement
Add Comment
Please, Sign In to add comment