LitresBookDescription

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

You will need to install an extension such as Tampermonkey to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला 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();

})();