The script adds a button to the site to generate the book description in the form of empty FB2 file
// ==UserScript==
// @name LitresBookDescription
// @name:ru LitresBookDescription
// @namespace 90h.yy.zz
// @version 0.1.1
// @author Ox90
// @match https://*.litres.ru/book/*
// @description The script adds a button to the site to generate the book description in the form of empty FB2 file
// @description:ru Скрипт добавляет кнопку формирования описания книги в виде пустого fb2 файла
// @require https://update.greasyfork.org/scripts/468831/1792771/HTML2FB2Lib.js
// @grant GM.xmlHttpRequest
// @run-at document-start
// @license MIT
// ==/UserScript==
(function() {
"use strict";
const PROGRAM_NAME = "LitresBookDescription";
const PROGRAM_VERSION = GM_info.script.version;
let mainButton = null;
function init() {
// Активация внешних стилей: используются псевдоэлементы
addStyles();
// Первоначальное добавление кнопки
addMainButton();
// Запуск отслеживания страницы, чтобы добавить кнопку после динамического обновления страницы
watchPage('#__next', addMainButton);
}
function watchPage(selector, handler) {
const container = document.querySelector(selector);
if (container) {
(new MutationObserver(function(mutations, observer) {
try {
if (handler(container)) observer.disconnect();
} catch (err) {
console.error(err);
}
})).observe(container, { childList: true, subtree: true });
}
}
function addMainButton() {
const container = document.querySelector(
'#main div[data-testid="book-card__wrapper"] div[data-analytics-id="book-characteristics"]'
);
if (!container) return;
if (!mainButton) {
mainButton = document.createElement('div');
mainButton.classList.add('lbd-main-button');
mainButton.innerHTML = '<div class="lbd-title"><span role="heading" aria-level="5">Метаданные книги: </span></div>' +
'<div role="list"><span class="lbd-value" role="listitem"><a tabindex="0" class="lbd-start-link">fb2</a></span></div>' +
'<div><span class="lbd-progress-message lbd-hidden">Ждите...</span></div>' +
'<div><a tabindex="0" class="lbd-result-link lbd-hidden" download="bookDescription.fb2">Скачать</a></div>';
mainButton.querySelector('.lbd-start-link').addEventListener('click', event => {
event.preventDefault();
extractDiscription();
});
}
container.append(mainButton);
return true;
}
function updateMainButton(content) {
const list = mainButton.querySelector('div[role=list]');
const prog = mainButton.querySelector('.lbd-progress-message');
const link = mainButton.querySelector('.lbd-result-link');
let target = null;
if (!content) {
target = list;
} else if (content.startsWith('blob:')) {
link.href = content;
target = link;
} else {
prog.textContent = content;
target = prog;
}
[ list, prog, link ].forEach(el => {
if (el === target) {
el.classList.remove('lbd-hidden');
} else {
el.classList.add('lbd-hidden');
}
});
if (target != link && link.href) {
URL.revokeObjectURL(link.href);
link.href = '';
}
}
async function extractDiscription() {
updateMainButton("Ждите...");
try {
const bookData = await loadFromJsonLd();
await extractHtmlData(bookData);
makeBookFile(bookData);
} catch (err) {
updateMainButton('Ошибка: ' + err.message);
if (!(err instanceof AppError)) {
console.error(err);
}
}
}
async function loadFromJsonLd() {
const bdata = {};
let found = false;
for (const node of Array.from(document.querySelectorAll('script[type="application/ld+json"]'))) {
try {
const data = JSON.parse(node.textContent);
if (data['@type'] === 'Book') {
bdata.title = data.name.trim();
if (data.isbn) bdata.isbn = data.isbn.trim();
if (data.description) bdata.annotation = data.description;
bdata.authors = (data.author[0] ? data.author : [ data.author ]).map(a => new FB2Author(a.name));
found = true;
break;
}
} catch (err) {
}
}
if (!found) throw new AppError('Не найден json+ld блок');
return bdata;
}
async function extractHtmlData(bdata) {
const bookEl = document.querySelector('[itemtype="https://schema.org/Book"]');
if (!bookEl) throw new AppError('Не найден блок информации о книге');
const properties = new Map();
{
const bookDt = bookEl.querySelector('div[data-testid="book-characteristics__wrapper"]');
if (!bookDt) throw new AppError('Не найден блок характеристик книги');
let itemEl = bookDt.firstElementChild;
while (itemEl) {
if (itemEl.children.length == 2) {
const r = /\s*(.+)\s*:/.exec(itemEl.children[0].textContent);
if (r) properties.set(r[1], itemEl.children[1]);
}
itemEl = itemEl.nextElementSibling;
}
}
function handleMetadataItem(name, handler) {
const el = properties.get(name);
handler(el);
}
// Переводчик
handleMetadataItem('Переводчик', (el) => {
if (!el) return;
bdata.translators = Array.from(el.querySelectorAll('a')).reduce((list, el) => {
const trName = el.textContent.trim();
if (trName) {
const tr = new FB2Author(trName);
tr.name = 'translator';
list.push(tr);
}
return list;
}, []);
});
// Ключевые слова
bdata.keywords = Array.from(bookEl.querySelectorAll('div[data-testid="book-genres-and-tags__wrapper"] a')).reduce((list, el) => {
const kw = el.textContent.trim();
if (kw) list.push(kw);
return list;
}, []);
// Жанры
bdata.genres = new FB2GenreList(bdata.keywords);
// Серия
bdata.sequence = (() => {
let el = bookEl.querySelector('div[data-testid="art__inSeries--title"] a');
if (el) {
let r = /«\s*(.+)\s*»/.exec(el.textContent);
if (r) {
const seq = { name: r[1] };
el = el.parentElement.firstChild;
if (el && el.nodeName === '#text') {
r = /(\d+)\D+\d+/.exec(el.textContent);
if (r) seq.number = Number(r[1]);
}
return seq;
}
}
})();
// Дата публикации книги
handleMetadataItem('Дата выхода на Литрес', (el) => {
if (!el) return;
const r = /(\d+)\s+([\S]+)\s+(\d+)/.exec(el.textContent);
if (r) {
const month = [ 'января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря' ].indexOf(r[2]) + 1;
if (month > 0) bdata.bookDate = new Date(`${r[3]}-${month}-${r[1]}`);
}
});
// Ссылка на источник
bdata.sourceURL = document.location.toString();
// Обложка книги
let el = bookEl.querySelector('div[data-testid="book-cover__wrapper"] img');
if (el && el.src) {
const img = new FB2Image(el.src);
console.log('Загрузка изображения...');
await img.load();
if (img.type === "image/webp") {
console.log('Конвертация изображения...');
await img.convert("image/jpeg");
}
img.id = "cover" + img.suffix();
bdata.coverpage = img;
}
// Правообладатель
handleMetadataItem('Правообладатель', (el) => {
if (!el) return;
const val = el.textContent.trim();
if (val) bdata.publisher = val;
});
}
function makeBookFile(bdata) {
const doc = new FB2Document();
doc.bookTitle = bdata.title;
doc.bookAuthors = bdata.authors;
doc.genres = bdata.genres;
doc.keywords = bdata.keywords;
doc.sequence = bdata.sequence;
doc.bookDate = bdata.bookDate;
doc.sourceURL = bdata.sourceURL;
doc.coverpage = bdata.coverpage;
doc.binaries.push(bdata.coverpage);
doc.annotation = new FB2Annotation();
doc.annotation.children.push(new FB2Paragraph(bdata.annotation));
if (bdata.translators && bdata.translators[0]) {
doc.translator = bdata.translators[0];
}
doc.idPrefix = 'lbd_';
doc.history.push("v1.0 - создание файла");
doc.publishBookTitle = bdata.title;
doc.isbn = bdata.isbn;
doc.publisher = bdata.publisher;
if (bdata.bookDate) doc.publishYear = bdata.bookDate.getFullYear();
doc.publishSequence = bdata.sequence;
// Фейковая глава для валидации
const chapter = new FB2Chapter();
chapter.children.push(new FB2EmptyLine());
doc.chapters.push(chapter);
// Должно быть text/plain, но в этом случае мобильный Firefox к имени файла добавляет .txt
const urlObj = URL.createObjectURL(new Blob([ doc ], { type: 'application/octet-stream' }));
updateMainButton(urlObj);
}
//-------------------------
class AppError extends Error {
}
//-------------------------
function addStyle(css) {
const style = document.getElementById("lbd_styles") || (function() {
const style = document.createElement('style');
style.type = 'text/css';
style.id = "lbd_styles";
document.head.appendChild(style);
return style;
})();
const sheet = style.sheet;
sheet.insertRule(css, (sheet.rules || sheet.cssRules || []).length);
}
function addStyles() {
[
'.lbd-main-button { font:normal 14px/20px var(--main-font,sans-serif); letter-spacing:.25px; display:flex; }',
'.lbd-title { min-width:205px; color:#9d9c9f }',
'.lbd-title span { display:flex; }',
'.lbd-title span::after { display:block; flex-grow:1; content:""; border-bottom:1px solid #ebebeb; }',
'.lbd-value { white-space:pre; }',
'.lbd-start-link { color:var(--link,#3d3dc7); text-decoration:none; cursor:pointer; outline:none; transition:color 80ms ease-in-out; }',
'.lbd-start-link:hover { color:rgba(61,61,199,.6); }',
'.lbd-progress-message { color: rgb(157, 156, 159); }',
'.lbd-hidden { display:none !important; }'
].forEach(s => addStyle(s));
}
// Запускает скрипт после загрузки страницы сайта
if (document.readyState === "loading") window.addEventListener("DOMContentLoaded", init);
else init();
})();