您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Экспорт диапазона глав в один FB2 (с шапкой). Починка выборки, юникода.
// ==UserScript== // @name Re-Library FB2: Bulk Export Novel // @namespace https://re-library.com/ // @version 1.31 // @description Экспорт диапазона глав в один FB2 (с шапкой). Починка выборки, юникода. // @match https://re-library.com/translations/* // @grant none // ==/UserScript== (() => { 'use strict'; if (document.getElementById('fb2-export-panel')) return; const h1 = document.querySelector('h1.entry-title'); const entry = document.querySelector('.entry-content'); if (!h1 || !entry) return; const novelTitle = (h1.textContent || '').trim(); const novelDesc = (() => { // краткая аннотация: первые 1–3 абзаца страницы романа const ps = [...entry.querySelectorAll('p')].slice(0, 3).map(p => p.textContent.trim()).filter(Boolean); return ps.join('\n\n'); })(); // собираем ссылки на главы только из TOC; отфильтровываем соц.шеры и якоря const chapterLinks = [...entry.querySelectorAll('a[href*="/chapter-"]')] .map(a => a.href.split('#')[0]) .filter(href => !href.includes('?share=')) .filter((v, i, arr) => arr.indexOf(v) === i); // уникальные if (chapterLinks.length === 0) return; // ==== UI ==== const panel = document.createElement('div'); panel.id = 'fb2-export-panel'; panel.style.cssText = 'margin:12px 0;padding:12px;border:1px solid #ddd;border-radius:8px;'; const fromSel = document.createElement('select'); const toSel = document.createElement('select'); fromSel.style.marginRight = '8px'; toSel.style.marginRight = '8px'; chapterLinks.forEach((url, i) => { const label = url.replace(/\/$/, '').split('/').pop(); const o1 = new Option(`#${i + 1} ${label}`, String(i)); const o2 = new Option(`#${i + 1} ${label}`, String(i)); fromSel.add(o1); toSel.add(o2); }); toSel.selectedIndex = chapterLinks.length - 1; const btn = document.createElement('button'); btn.textContent = 'Экспорт FB2 (диапазон)'; btn.style.cssText = 'padding:6px 12px;font-weight:600;'; const barWrap = document.createElement('div'); barWrap.style.cssText = 'margin-top:10px;'; const bar = document.createElement('div'); bar.style.cssText = 'height:10px;background:#eee;border-radius:6px;overflow:hidden;'; const fill = document.createElement('div'); fill.style.cssText = 'height:100%;width:0%;transition:width .25s;'; bar.appendChild(fill); const status = document.createElement('div'); status.style.cssText = 'margin-top:6px;font-size:12px;opacity:.85;'; barWrap.append(bar, status); panel.append('С главы: ', fromSel, ' по: ', toSel, ' ', btn, barWrap); h1.parentNode.insertBefore(panel, h1.nextSibling); // ==== helpers ==== function sanitizeText(s) { return (s || '') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, ''') .replace(/……/g, '......') .replace(/…/g, '...') .replace(/[–—]/g, '-') .replace(/[“”«»]/g, '"') .replace(/\u00A0/g, ' ') .replace(/[\u2000-\u200B]/g, ' ') .replace(/\s+\n/g, '\n') .trim(); } function cleanContainer(root) { const junkSel = [ '.navigation', '.sharedaddy', '.heateor_sss_sharing_container', '.wpulike', '.wpdiscuz', '.comments-area', '#comments', '.comment-respond', 'form.comment-form', '.sd-sharing', '.tags-links', 'script', 'style', 'iframe', 'ins', 'nav', 'aside' ].join(','); root.querySelectorAll(junkSel).forEach(n => n.remove()); } function extractParagraphs(container) { const paras = []; cleanContainer(container); const stop = container.querySelector('.navigation'); for (const p of container.querySelectorAll('p')) { if (stop) { const rel = p.compareDocumentPosition(stop); if (!(rel & Node.DOCUMENT_POSITION_FOLLOWING)) break; // дошли до/после навигации — стоп } const t = (p.textContent || '').trim(); if (!t) continue; if (/^leave a comment$/i.test(t)) continue; if (/^link here$/i.test(t)) continue; if (/donation to this page helps/i.test(t)) continue; if (/support your favorite translation groups/i.test(t)) continue; paras.push(`<p>${sanitizeText(t)}</p>`); } return paras; } async function fetchChapter(url) { const res = await fetch(url, { credentials: 'same-origin' }); const html = await res.text(); const doc = new DOMParser().parseFromString(html, 'text/html'); const h = doc.querySelector('h1.entry-title'); const cont = doc.querySelector('.entry-content'); if (!h || !cont) return { title: url, section: '' }; const title = (h.textContent || '').trim(); const body = extractParagraphs(cont).join('\n'); const section = `<section>\n<title><p>${sanitizeText(title)}</p></title>\n${body}\n</section>`; return { title, section }; } function updateProgress(i, total, label) { const pct = Math.round(((i) / total) * 100); fill.style.width = pct + '%'; status.textContent = `${i}/${total} — ${label || ''}`; } function buildFB2(sections) { return `<?xml version="1.0" encoding="UTF-8"?> <FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0"> <description> <title-info> <book-title>${sanitizeText(novelTitle)}</book-title> ${novelDesc ? `<annotation><p>${sanitizeText(novelDesc)}</p></annotation>` : ''} </title-info> </description> <body> ${sections.join('\n')} </body> </FictionBook>`; } function download(text, name) { const blob = new Blob([text], { type: 'application/xml;charset=utf-8' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = name; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 4000); } // ==== export ==== btn.onclick = async () => { const from = Math.min(+fromSel.value, +toSel.value); const to = Math.max(+fromSel.value, +toSel.value); const list = chapterLinks.slice(from, to + 1); const sections = []; for (let i = 0; i < list.length; i++) { try { const { title, section } = await fetchChapter(list[i]); updateProgress(i + 1, list.length, title); if (section) sections.push(section); } catch (e) { console.error('Fetch failed:', list[i], e); updateProgress(i + 1, list.length, 'Ошибка загрузки'); } } const fb2 = buildFB2(sections); const safe = novelTitle.replace(/[^\w\d\-]+/g, '_').slice(0, 100); download(fb2, `${safe} (${from + 1}-${to + 1}).fb2`); }; })();