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.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();

})();