Некоторые книги, по требованию правообладателя, доступны только для чтения с сайта или в приложениях ЛитРес. Все бы ничего, но бывают такие ситуации:



Право читать обошлось в 2/3 от стоимости бумажного носителя, если брать с сайта издательства.


Справедливости нет. есть только я
Смерть

И тут я решил написать grabber.


За основу взял QWebEngineView, что бы не заморачиваться с авторизацией. И внешне это выглядит так:



Sharing куков между QNetworkAccessManager и QWebEngineView


Для этого в Qt есть QWebEngineCookieStore и
QNetworkCookieJar


MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    m_ui(new Ui::MainWindow),
    m_store(nullptr),
    m_cookieJar(new QNetworkCookieJar (this)),
    m_networmManager(new QNetworkAccessManager(this)),
    m_try(0),
    m_currentPage(0),
    m_capches(1)
{
    m_ui->setupUi(this);

    m_store = m_ui->webView->page()->profile()->cookieStore();
    Q_ASSERT(m_store != nullptr);
    connect(m_store, &QWebEngineCookieStore::cookieAdded, this, &MainWindow::handleCookieAdded);
    m_store->loadAllCookies();
    m_ui->webView->load(QUrl("https://www.litres.ru/"));
    m_networmManager->setCookieJar(m_cookieJar);

    connect(m_networmManager, &QNetworkAccessManager::finished,
            this, &MainWindow::handleImage);
}

void MainWindow::handleCookieAdded(const QNetworkCookie &cookie)
{
    m_cookieJar->insertCookie(cookie);
}

Когда переходим на чтение книги и нажимаем на кнопку Grab, то берется url вида:


https://www.litres.ru/static/or3/view/or.html?art_type=4&file=26599915&bname=Разработка веб-приложений в ReactJS&cover=%2Fstatic%2Fbookimages%2F26%2F59%2F99%2F26599923.bin.dir%2F26599923.cover.jpg&art=22880082&user=Что-то&uuid=Что-то

Вытаскиваем id файла и название:


void MainWindow::onGrabButtonClicked()
{
    if(!parseUrl(m_ui->webView->url()))
    {
        return;
    }

    const auto paths = QStandardPaths::standardLocations(QStandardPaths::DownloadLocation);
    if (paths.isEmpty()) {
        qWarning()<<"There is no standard path to download";
        return;
    }
    downloadTo(*paths.begin());
}

bool MainWindow::parseUrl(const QUrl &url)
{
    const auto query = QUrlQuery(url.query(QUrl::FullyDecoded));
    if (query.isEmpty()){
        return false;
    }

    static const QVector<QString> fields = {
        "file", "bname", "uuid"
    };

    for (const auto& key: fields) {
        if (!query.hasQueryItem(key)) {
            qWarning()<<"Query hasn't param"<< key;
            return false;
        }
    }

    m_name = query.queryItemValue("bname", QUrl::FullyDecoded);
    m_file = query.queryItemValue("file");
    m_format = "jpg";

    return true;
}

MainWindow::downloadTo настраивает QPdfWriter и QPainter


void MainWindow::downloadTo(const QString &path)
{
    QDir dir(path);

    m_writer = std::make_unique<QPdfWriter>(dir.absoluteFilePath(m_name+".pdf"));
    QPageLayout layout(QPageSize(QPageSize::A4), QPageLayout::Portrait,
                       QMarginsF(0,0,0,0));

    m_writer->setPageLayout(layout);
    m_writer->setResolution(96);
    m_writer->setTitle(m_name);
    m_painter = std::make_unique<QPainter>();
    m_painter->begin(m_writer.get());

    nextImage();
}

Скачивание страницы


Страницы скачиваются по url вида:


https://www.litres.ru/pages/read_book_online/?file=26599915&page=2&rt=w1280&ft=gif

Параметр Описание
rt отвечает за размеры, принимает значение w640, w1280
ft формат gif или jpg
page номер страницы
file идентификатор файла

Формат jpg применяется для страниц с графикой, в то же время gif для текста.
Если страницы по url: https://www.litres.ru/pages/read_book_online/?file=26599915&page=0&rt=w1280&ft=gif не существует, то следует запросить https://www.litres.ru/pages/read_book_online/?file=26599915&page=0&rt=w1280&ft=jpg


Получаем:


void MainWindow::nextImage()
{
    QUrlQuery query;
    query.addQueryItem("file", m_file);
    query.addQueryItem("rt", "w640");
    query.addQueryItem("ft", m_format);
    query.addQueryItem("page", QString::number(m_currentPage));

    QUrl url(BasePath);
    url.setQuery(query);
    m_networmManager->get(QNetworkRequest(url));
    ++m_currentPage;
}

void MainWindow::handleImage(QNetworkReply *reply)
{
    reply->deleteLater();

    if (reply->error() != QNetworkReply::NoError) {
        qWarning()<<"Network error"<<reply->errorString();
        if(m_try == 3) {
            m_painter->end();
            m_painter.reset();
            m_writer.reset();
            return;
        }

        if (m_format == "gif") {
            m_format = "jpg";
        } else {
            m_format = "gif";
        }
        --m_currentPage;
        ++m_try;
        nextImage();
        return;
    }
    m_try = 0;

    qDebug()<<"Write page"<<m_currentPage<<reply->url();
    std::string f;
    if (m_format == "jpg") {
        f = "JPEG";
    } else {
        f = "GIF";
    }
    const auto data = reply->readAll();
    const auto source = QImage::fromData(data, f.c_str());
    if (source.isNull()) {
        //handleCapcha(data, reply->url());
        --m_currentPage;
        nextImage();
        return;
    }

    m_ui->pages->setText(QString::number(m_currentPage));
    const auto dest = source.scaledToWidth(m_writer->width()/*, Qt::SmoothTransformation */);
    m_painter->drawImage(QPoint(0,0), dest);
    m_writer->newPage();

    nextImage();
}

Капча


Капча вроде бы есть, но в тоже время нет. Выскакивает не всегда


Мы заметили странную активность с вашего компьютера. Возможно, мы ошиблись, и эта активность идёт не от вас. В таком случае, подтвердите, что вы не робот и продолжайте пользоваться нашим сайтом.

Оказалось, что можно просто перезапросить страницу и дальше продолжить скачивание изображений. Если же вам не нравится прикидываться роботом, то можно это обработать:


void MainWindow::handleCapcha(const QByteArray &page, const QUrl &url )
{
    ++m_capches;
    m_ui->webView->page()->setHtml(page, url);
    m_ui->captches->setText(QString::number(m_capches));
    QEventLoop loop;
    constexpr int duration = 1000*60*5;
    QTimer::singleShot(duration, &loop, &QEventLoop::quit);
    loop.exec();
}

Тут загружаем в WebView страницу с капчей. После чего, можем ввести капчу.


Итого


Книга объемом 256 страниц в PDF со страницами A4 и DPI 96 весит 51,7 МБ против 5,8 МБ зашифрованного документа.


Код доступен на GitHubGist

Поделиться с друзьями
-->

Комментарии (13)


  1. pvp
    31.07.2017 11:34
    +4

    «мы не наивные люди, это тоже предусмотрено: в видеобуфере будет мусор.»

    (с) http://users.livejournal.com/magister-/238369.html?thread=2093089#t2093089


    1. RPG18
      31.07.2017 12:25
      +3

      Особенно насмешила капча. Так и представил в голове:


      • Возможно вы робот?
      • Да.
      • Продолжайте дальше.


  1. redmanmale
    31.07.2017 12:56
    +1

    Ещё бы прикрутить Tesseract, экспорт в FB2 и загрузку в либрусек.


    1. Evengard
      31.07.2017 19:43
      +1

      Не либрусек а флибуста) Либрусек давно под гнётом правоохренителей


      1. Faramant
        01.08.2017 10:50

        А вы какой Либрусек имеете ввиду, новый или старый?


        1. Evengard
          01.08.2017 17:57

          Кхм есть какой-то «новый»? Я думал флибуста и есть наследие либрусека. Про «новый» не слышал, знаю только про «старый».


          1. Faramant
            02.08.2017 04:48

            Новый librusec точка pro. Если я правильно помню, там база пользователей сохранена была со старого. А теперь и старый сайт поднялся с прежним функционалом. Флибусту нельзя назвать наследницей, она параллельной Либрусеку была всегда.


  1. MeyerHoffman
    31.07.2017 14:55
    +4

    У Литреса в мобильном приложении можно читать нормальные PDF. Если записать трафик через wireshark, например, то можно получить прямую ссылку на PDF файл и не мучится с картинками в PDF.


  1. kovserg
    01.08.2017 10:50

    Почему в pdf для этого же есть djvu


    1. RPG18
      01.08.2017 10:51

      Потому что PDF в Qt есть из коробки


  1. dranikus
    01.08.2017 12:57

    А что за проблема с отображением книги на Литрес была? Как ее воспроизвести? Это на сайте было или в приложении?


    1. RPG18
      01.08.2017 12:58

      Приложение под Android. Отправил разработчикам логи со скринами.


      1. dranikus
        01.08.2017 13:15

        Спасибо!