Lettore EPUB nativo: ReaderPage (WebView+capitoli+progresso), segno locale in QSettings, sync POST /ajax/bookmark con fallback CSRF, pulsante Leggi nel dettaglio
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
#include <QFileInfo>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QRegularExpression>
|
||||
#include <QSslError>
|
||||
#include <QStandardPaths>
|
||||
#include <QTimer>
|
||||
@@ -134,6 +135,90 @@ static bool looksLikeImage(const QByteArray &data)
|
||||
|| data.startsWith("GIF8") || data.startsWith("RIFF");
|
||||
}
|
||||
|
||||
void ApiClient::postBookmark(int bookId, const QString &format, const QString &key)
|
||||
{
|
||||
const QString url = m_settings->baseUrl() + QStringLiteral("/ajax/bookmark/")
|
||||
+ QString::number(bookId) + QLatin1Char('/') + format;
|
||||
qDebug() << "postBookmark:" << url << "key=" << key;
|
||||
|
||||
const QUrl requestUrl(url);
|
||||
QNetworkRequest request(requestUrl);
|
||||
request.setRawHeader("User-Agent", "harbour-calibreweb/0.1");
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader,
|
||||
QLatin1String("application/x-www-form-urlencoded"));
|
||||
applyAuth(request);
|
||||
const QByteArray body = "bookmark=" + QUrl::toPercentEncoding(key);
|
||||
|
||||
QNetworkReply *reply = m_nam.post(request, body);
|
||||
if (m_settings->ignoreSslErrors()) {
|
||||
connect(reply, &QNetworkReply::sslErrors, reply,
|
||||
static_cast<void (QNetworkReply::*)(const QList<QSslError> &)>(
|
||||
&QNetworkReply::ignoreSslErrors));
|
||||
}
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, url, key]() {
|
||||
reply->deleteLater();
|
||||
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
qDebug() << "postBookmark status:" << status;
|
||||
// 400/403 = probabile CSRF attivo: recupera il token dalla pagina di login e riprova
|
||||
if ((status == 400 || status == 403) && !m_csrfRetried) {
|
||||
m_csrfRetried = true;
|
||||
fetchCsrfTokenAndRetry(url, key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ApiClient::fetchCsrfTokenAndRetry(const QString &url, const QString &key)
|
||||
{
|
||||
const QUrl loginUrl(m_settings->baseUrl() + QStringLiteral("/login"));
|
||||
QNetworkRequest request(loginUrl);
|
||||
request.setRawHeader("User-Agent", "harbour-calibreweb/0.1");
|
||||
applyAuth(request);
|
||||
QNetworkReply *reply = m_nam.get(request);
|
||||
if (m_settings->ignoreSslErrors()) {
|
||||
connect(reply, &QNetworkReply::sslErrors, reply,
|
||||
static_cast<void (QNetworkReply::*)(const QList<QSslError> &)>(
|
||||
&QNetworkReply::ignoreSslErrors));
|
||||
}
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, url, key]() {
|
||||
reply->deleteLater();
|
||||
const QByteArray body = reply->readAll();
|
||||
// <input id="csrf_token" name="csrf_token" type="hidden" value="...">
|
||||
QRegularExpression re(QStringLiteral(
|
||||
"name=[\"']csrf_token[\"'][^>]*value=[\"']([^\"']+)[\"']"));
|
||||
const QRegularExpressionMatch match = re.match(QString::fromUtf8(body));
|
||||
if (match.hasMatch()) {
|
||||
postBookmarkWithCsrf(url, key, match.captured(1));
|
||||
} else {
|
||||
qDebug() << "postBookmark: token CSRF non trovato nella pagina di login";
|
||||
}
|
||||
m_csrfRetried = false;
|
||||
});
|
||||
}
|
||||
|
||||
void ApiClient::postBookmarkWithCsrf(const QString &url, const QString &key,
|
||||
const QString &csrfToken)
|
||||
{
|
||||
const QUrl requestUrl(url);
|
||||
QNetworkRequest request(requestUrl);
|
||||
request.setRawHeader("User-Agent", "harbour-calibreweb/0.1");
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader,
|
||||
QLatin1String("application/x-www-form-urlencoded"));
|
||||
request.setRawHeader("X-CSRFToken", csrfToken.toUtf8());
|
||||
applyAuth(request);
|
||||
const QByteArray body = "bookmark=" + QUrl::toPercentEncoding(key);
|
||||
QNetworkReply *reply = m_nam.post(request, body);
|
||||
if (m_settings->ignoreSslErrors()) {
|
||||
connect(reply, &QNetworkReply::sslErrors, reply,
|
||||
static_cast<void (QNetworkReply::*)(const QList<QSslError> &)>(
|
||||
&QNetworkReply::ignoreSslErrors));
|
||||
}
|
||||
connect(reply, &QNetworkReply::finished, this, [reply]() {
|
||||
reply->deleteLater();
|
||||
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
qDebug() << "postBookmark (con CSRF) status:" << status;
|
||||
});
|
||||
}
|
||||
|
||||
void ApiClient::fetchImage(const QString &url, const QString &id)
|
||||
{
|
||||
if (url.isEmpty()) {
|
||||
|
||||
@@ -21,6 +21,8 @@ public:
|
||||
|
||||
Q_INVOKABLE void getFeed(const QString &url);
|
||||
Q_INVOKABLE void fetchImage(const QString &url, const QString &id);
|
||||
// sincronizza la posizione di lettura su calibre-web (POST /ajax/bookmark)
|
||||
Q_INVOKABLE void postBookmark(int bookId, const QString &format, const QString &key);
|
||||
|
||||
signals:
|
||||
void feedReady(const QVariantList &entries, const QString &nextUrl, const QString &feedTitle);
|
||||
@@ -31,10 +33,14 @@ private:
|
||||
QNetworkReply *startGet(const QUrl &url);
|
||||
void applyAuth(QNetworkRequest &request);
|
||||
QString resolveUrl(const QString &relative) const;
|
||||
void postBookmarkWithCsrf(const QString &url, const QString &key,
|
||||
const QString &csrfToken);
|
||||
void fetchCsrfTokenAndRetry(const QString &url, const QString &key);
|
||||
|
||||
Settings *m_settings;
|
||||
QNetworkAccessManager m_nam;
|
||||
QString m_cacheDir;
|
||||
bool m_csrfRetried = false;
|
||||
};
|
||||
|
||||
#endif // APICLIENT_H
|
||||
|
||||
+17
-2
@@ -129,7 +129,8 @@ QString readTextFile(const QString &path)
|
||||
// EpubBook
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
EpubBook::EpubBook()
|
||||
EpubBook::EpubBook(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -189,7 +190,9 @@ bool EpubBook::open(const QString &epubPath)
|
||||
return false;
|
||||
if (!parseOpf())
|
||||
return false;
|
||||
return !m_chapters.isEmpty();
|
||||
const bool ok = !m_chapters.isEmpty();
|
||||
emit changed();
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool EpubBook::parseContainer()
|
||||
@@ -397,6 +400,18 @@ QString EpubBook::chapterTitle(int index) const
|
||||
return m_chapters.at(index).title;
|
||||
}
|
||||
|
||||
QVariantList EpubBook::chapterList() const
|
||||
{
|
||||
QVariantList list;
|
||||
for (int i = 0; i < m_chapters.size(); ++i) {
|
||||
QVariantMap m;
|
||||
m.insert(QStringLiteral("index"), i);
|
||||
m.insert(QStringLiteral("title"), m_chapters.at(i).title);
|
||||
list.append(m);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
QString EpubBook::makeBookmarkKey(int chapterIndex, double fraction) const
|
||||
{
|
||||
return QStringLiteral("cw:v1:%1:%2").arg(chapterIndex).arg(fraction, 0, 'f', 4);
|
||||
|
||||
+21
-10
@@ -2,7 +2,9 @@
|
||||
#define EPUB_H
|
||||
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVariantList>
|
||||
|
||||
struct EpubChapter
|
||||
{
|
||||
@@ -11,27 +13,36 @@ struct EpubChapter
|
||||
QString title;
|
||||
};
|
||||
|
||||
class EpubBook
|
||||
class EpubBook : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(QString title READ title NOTIFY changed)
|
||||
Q_PROPERTY(QString author READ author NOTIFY changed)
|
||||
Q_PROPERTY(int chapterCount READ chapterCount NOTIFY changed)
|
||||
|
||||
public:
|
||||
EpubBook();
|
||||
explicit EpubBook(QObject *parent = 0);
|
||||
~EpubBook();
|
||||
|
||||
bool open(const QString &epubPath);
|
||||
void close();
|
||||
Q_INVOKABLE bool open(const QString &epubPath);
|
||||
Q_INVOKABLE void close();
|
||||
|
||||
QString title() const { return m_title; }
|
||||
QString author() const { return m_author; }
|
||||
QList<EpubChapter> chapters() const { return m_chapters; }
|
||||
QString baseDir() const { return m_baseDir; }
|
||||
int chapterCount() const { return m_chapters.size(); }
|
||||
|
||||
QString chapterFilePath(int index) const;
|
||||
QString chapterTitle(int index) const;
|
||||
Q_INVOKABLE QString chapterFilePath(int index) const;
|
||||
Q_INVOKABLE QString chapterTitle(int index) const;
|
||||
// elenco [{index, title}] per la lista capitoli
|
||||
Q_INVOKABLE QVariantList chapterList() const;
|
||||
|
||||
// chiave segno locale, es. "cw:v1:3:0.42" (capitolo 3, 42% del capitolo)
|
||||
QString makeBookmarkKey(int chapterIndex, double fraction) const;
|
||||
bool parseBookmarkKey(const QString &key, int *chapterIndex, double *fraction) const;
|
||||
Q_INVOKABLE QString makeBookmarkKey(int chapterIndex, double fraction) const;
|
||||
Q_INVOKABLE bool parseBookmarkKey(const QString &key, int *chapterIndex, double *fraction) const;
|
||||
|
||||
signals:
|
||||
void changed();
|
||||
|
||||
private:
|
||||
bool extractZip(const QString &epubPath);
|
||||
|
||||
+4
-1
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "apiclient.h"
|
||||
#include "downloader.h"
|
||||
#include "epub.h"
|
||||
#include "settings.h"
|
||||
|
||||
Q_DECL_EXPORT int main(int argc, char *argv[])
|
||||
@@ -13,7 +14,9 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
|
||||
QGuiApplication *app = SailfishApp::application(argc, argv);
|
||||
QQuickView *view = SailfishApp::createView();
|
||||
|
||||
qDebug() << "harbour-calibreweb v0.1.0 build" << __DATE__ << __TIME__;
|
||||
qDebug() << "harbour-calibreweb v0.2.0 build" << __DATE__ << __TIME__;
|
||||
|
||||
qmlRegisterType<EpubBook>("harbour.calibreweb", 1, 0, "EpubBook");
|
||||
|
||||
Settings settings;
|
||||
ApiClient apiClient(&settings);
|
||||
|
||||
@@ -87,3 +87,22 @@ QString Settings::baseUrl() const
|
||||
url.chop(1);
|
||||
return url;
|
||||
}
|
||||
|
||||
QString Settings::bookmarkKey(int bookId, const QString &format) const
|
||||
{
|
||||
return m_settings.value(QStringLiteral("bookmarks/%1/%2").arg(bookId).arg(format))
|
||||
.toString();
|
||||
}
|
||||
|
||||
void Settings::setBookmarkKey(int bookId, const QString &format, const QString &key)
|
||||
{
|
||||
m_settings.setValue(QStringLiteral("bookmarks/%1/%2").arg(bookId).arg(format), key);
|
||||
}
|
||||
|
||||
QString Settings::bookFilePath(int bookId, const QString &format) const
|
||||
{
|
||||
const QString dir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
|
||||
+ QStringLiteral("/books");
|
||||
QDir().mkpath(dir);
|
||||
return dir + QStringLiteral("/%1.%2").arg(bookId).arg(format);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,13 @@ public:
|
||||
// URL del server senza slash finale
|
||||
QString baseUrl() const;
|
||||
|
||||
// --- segnalibri (posizione di lettura, per libro+formato) ---
|
||||
Q_INVOKABLE QString bookmarkKey(int bookId, const QString &format) const;
|
||||
Q_INVOKABLE void setBookmarkKey(int bookId, const QString &format, const QString &key);
|
||||
|
||||
// path del file libro scaricato in cache (per il lettore)
|
||||
Q_INVOKABLE QString bookFilePath(int bookId, const QString &format) const;
|
||||
|
||||
signals:
|
||||
void serverUrlChanged();
|
||||
void usernameChanged();
|
||||
|
||||
Reference in New Issue
Block a user