LitresBookDescription

The script adds a button to the site to generate the book description in the form of empty FB2 file

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name           LitresBookDescription
// @name:ru        LitresBookDescription
// @namespace      90h.yy.zz
// @version        0.1.2
// @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 loadFromJson();
    await extractHtmlData(bookData);
    await makeBookFile(bookData);
  } catch (err) {
    updateMainButton('Ошибка: ' + err.message);
    if (!(err instanceof AppError)) {
      console.error(err);
    }
  }
}

async function loadFromJson() {
  const bdata = {};
  let found = false;
  let jdata = null;

  for (const node of Array.from(document.querySelectorAll('script[type="application/ld+json"]'))) {
    try {
      jdata = JSON.parse(node.textContent);
    } catch (err) {
    }

    if (jdata['@type'] === 'Book') {
      bdata.title = jdata.name && jdata.name.trim() || 'Без названия';
      if (!jdata.author) {
        bdata.authors = [ new FB2Author('Неизвестный') ];
      } else {
        bdata.authors = (jdata.author[0] ? jdata.author : [ jdata.author ]).map(a => new FB2Author(a.name));
      }
      if (jdata.isbn) bdata.isbn = jdata.isbn.trim();
      if (jdata.description) bdata.annotation = `<p>${jdata.description}</p>`;

      found = true;
      break;
    }
  }

  if (!found) throw new AppError('Не найден json+ld блок');
  console.log('Обнаружен блок json+ld');

  let el = document.getElementById('__NEXT_DATA__');
  if (!el) throw new AppError('Не найден элемент NEXT_DATA');

  try {
    jdata = JSON.parse(el.textContent);
  } catch (err) {
    throw new AppError('Неожиданный формат next_data');
  }
  console.log('Обнаружен блок next_data');

  if (!jdata.props || !jdata.props.pageProps || !jdata.props.pageProps.artIdOrSlug) {
    throw new AppError('Id книги не найден');
  }
  bdata.bookId = jdata.props.pageProps.artIdOrSlug;
  console.log('Id книги: ' + bdata.bookId);

  try {
    jdata = JSON.parse(jdata.props.pageProps.initialState);
  } catch (err) {
    throw new AppError('Неожиданный формат initialState');
  }
  console.log('initialState loaded');

  if (!jdata.rtkqApi || !jdata.rtkqApi.queries) {
    throw new AppError('Не найден объект queries');
  }
  jdata = jdata.rtkqApi.queries;

  const query = `getArtData({"artIdOrSlug":${bdata.bookId}})`;
  if (jdata[query] && jdata[query].data) {
    const htmlAnn = jdata[query].data.html_annotation;
    if (htmlAnn) {
      bdata.annotation = htmlAnn;
      console.log('Обнаружена HTML аннотация');
    }
  }

  return bdata;
}

async function extractHtmlData(bdata) {
  console.log('Анализ DOM дерева страницы');

  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;
  });
}

async function makeBookFile(bdata) {
  console.log('Формирование документа FB2');

  const doc = new FB2Document();
  doc.bookTitle = bdata.title;
  doc.bookAuthors = bdata.authors;
  if (bdata.translators && bdata.translators[0]) {
    doc.translator = bdata.translators[0];
  }
  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.idPrefix = 'lbd_';
  doc.history.push("v1.0 - создание файла");

  if (bdata.annotation) {
    doc.bindParser('a', new FB2AnnotationParser());
    const fragment = document.createElement('div');
    fragment.innerHTML = bdata.annotation;
    await doc.parse('a', fragment, false);
    doc.bindParser();
  }

  doc.publishBookTitle = bdata.title;
  doc.isbn = bdata.isbn;
  doc.publisher = bdata.publisher;
  if (bdata.bookDate) doc.publishYear = bdata.bookDate.getFullYear();
  doc.publishSequence = bdata.sequence;

  if (doc.publisher === 'Автор') {
    if (!doc.genres.find(g => g.value === 'network_literature')) {
      doc.genres.push(new FB2Genre('network_literature'));
    }
  }

  // Фейковая глава для валидации
  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();

})();