LitresBookDescription

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();

})();