Rimossa la sync server dell'avanzamento di lettura (login di sessione, postBookmark, fetchBookmark): resta solo il salvataggio locale della posizione (QSettings) con ripristino locale all'apertura. ApiClient riportato a feed OPDS + copertine
This commit is contained in:
@@ -15,7 +15,6 @@ Page {
|
||||
property bool ready: false
|
||||
property bool restoring: false
|
||||
property int saveCounter: 0
|
||||
property int syncCounter: 0
|
||||
property string errorMessage: ""
|
||||
// reflow: scala il font (il testo si ri-avvolge), non lo zoom grafico
|
||||
property double fontScale: 1.0
|
||||
@@ -36,10 +35,6 @@ Page {
|
||||
text: "Indice capitoli"
|
||||
onClicked: showChapterList()
|
||||
}
|
||||
MenuItem {
|
||||
text: "Aggiorna posizione sul server"
|
||||
onClicked: syncNow()
|
||||
}
|
||||
MenuItem {
|
||||
text: "Margini del testo"
|
||||
onClicked: {
|
||||
@@ -201,20 +196,11 @@ Page {
|
||||
if (bookId <= 0)
|
||||
return
|
||||
page.saveCounter++
|
||||
// salva locale ogni ~10 s, sync server ogni ~30 s
|
||||
// salva in locale ogni ~10 s (o subito su forzatura)
|
||||
if (force || page.saveCounter % 5 === 0) {
|
||||
appSettings.setBookmarkKey(page.bookId, page.bookFormat,
|
||||
book.makeBookmarkKey(page.currentChapter, page.chapterFraction))
|
||||
}
|
||||
if (force || page.syncCounter++ % 15 === 0)
|
||||
syncNow()
|
||||
}
|
||||
|
||||
function syncNow() {
|
||||
if (bookId <= 0)
|
||||
return
|
||||
apiClient.postBookmark(page.bookId, page.bookFormat,
|
||||
book.makeBookmarkKey(page.currentChapter, page.chapterFraction))
|
||||
}
|
||||
|
||||
function gotoChapter(index) {
|
||||
@@ -315,11 +301,6 @@ Page {
|
||||
page.currentChapter = ch
|
||||
page.chapterFraction = fr
|
||||
loadChapter()
|
||||
|
||||
// ripristino anche dal server: la posizione remota vince, se esiste.
|
||||
// Il login di sessione viene fatto qui (prima richiesta verso le route web)
|
||||
if (page.bookId > 0)
|
||||
apiClient.fetchBookmark(page.bookId, page.bookFormat)
|
||||
}
|
||||
|
||||
onStatusChanged: {
|
||||
@@ -332,33 +313,4 @@ Page {
|
||||
applyMarginsJS()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: apiClient
|
||||
|
||||
// posizione letta dal server all'apertura del libro
|
||||
onBookmarkReady: {
|
||||
if (bookId !== page.bookId)
|
||||
return
|
||||
if (key.length === 0)
|
||||
return // nessun segnalibro sul server: resta la posizione locale
|
||||
var r = book.parseBookmarkKey(key)
|
||||
if (!r || !r.hasOwnProperty("chapter"))
|
||||
return
|
||||
var same = (r.chapter === page.currentChapter)
|
||||
&& (Math.abs(r.fraction - page.chapterFraction) < 0.01)
|
||||
if (same)
|
||||
return
|
||||
page.currentChapter = r.chapter
|
||||
page.chapterFraction = r.fraction
|
||||
// aggiorna anche la copia locale
|
||||
appSettings.setBookmarkKey(page.bookId, page.bookFormat, key)
|
||||
if (page.ready && book.chapterCount() > 0)
|
||||
loadChapter()
|
||||
}
|
||||
// sync non disponibile (server irraggiungibile, login bloccato, ...):
|
||||
// si continua in locale, senza disturbare il lettore
|
||||
onBookmarkError: { }
|
||||
onLoginFailed: { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,12 @@
|
||||
#include "settings.h"
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QRegularExpression>
|
||||
#include <QSslError>
|
||||
#include <QStandardPaths>
|
||||
#include <QTimer>
|
||||
@@ -136,250 +134,6 @@ static bool looksLikeImage(const QByteArray &data)
|
||||
|| data.startsWith("GIF8") || data.startsWith("RIFF");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Login di sessione calibre-web (route web: richiedono flask-login, il Basic
|
||||
// auth serve solo per OPDS)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
QString ApiClient::extractCsrfToken(const QByteArray &body) const
|
||||
{
|
||||
// <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));
|
||||
return match.hasMatch() ? match.captured(1) : QString();
|
||||
}
|
||||
|
||||
void ApiClient::ensureLogin(const std::function<void()> &after)
|
||||
{
|
||||
if (m_loggedIn) {
|
||||
after();
|
||||
return;
|
||||
}
|
||||
if (m_loginInProgress) {
|
||||
m_loginQueue.append(after);
|
||||
return;
|
||||
}
|
||||
// accesso anonimo: nessun login possibile, esegui comunque (l'endpoint
|
||||
// deciderà); oppure cooldown dopo un login fallito (limiter server 3/min)
|
||||
if (m_settings->username().isEmpty()
|
||||
|| QDateTime::currentMSecsSinceEpoch() < m_loginCooldownUntil) {
|
||||
after();
|
||||
return;
|
||||
}
|
||||
|
||||
m_loginInProgress = true;
|
||||
m_loginQueue.append(after);
|
||||
|
||||
// 1. GET /login per il token CSRF
|
||||
const QUrl loginUrl(m_settings->baseUrl() + QStringLiteral("/login"));
|
||||
QNetworkRequest request(loginUrl);
|
||||
request.setRawHeader("User-Agent", "harbour-calibreweb/0.1");
|
||||
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]() {
|
||||
reply->deleteLater();
|
||||
const QString token = extractCsrfToken(reply->readAll());
|
||||
if (token.isEmpty()) {
|
||||
qDebug() << "login: token CSRF non trovato";
|
||||
finishLogin(false, QStringLiteral(
|
||||
"Login non disponibile: pagina di login non raggiungibile"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. POST del form standard calibre-web
|
||||
QNetworkRequest req(QUrl(m_settings->baseUrl() + QStringLiteral("/login")));
|
||||
req.setRawHeader("User-Agent", "harbour-calibreweb/0.1");
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader,
|
||||
QLatin1String("application/x-www-form-urlencoded"));
|
||||
applyAuth(req);
|
||||
const QByteArray body = "username=" + QUrl::toPercentEncoding(m_settings->username())
|
||||
+ "&password=" + QUrl::toPercentEncoding(m_settings->password())
|
||||
+ "&csrf_token=" + QUrl::toPercentEncoding(token)
|
||||
+ "&next=/&remember_me=on&submit=Submit";
|
||||
QNetworkReply *post = m_nam.post(req, body);
|
||||
if (m_settings->ignoreSslErrors()) {
|
||||
connect(post, &QNetworkReply::sslErrors, post,
|
||||
static_cast<void (QNetworkReply::*)(const QList<QSslError> &)>(
|
||||
&QNetworkReply::ignoreSslErrors));
|
||||
}
|
||||
connect(post, &QNetworkReply::finished, this, [this, post]() {
|
||||
post->deleteLater();
|
||||
const int status = post->attribute(
|
||||
QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
const QByteArray respBody = post->readAll();
|
||||
// successo: redirect a / (QNAM segue) e il body non contiene il
|
||||
// form di login; fallimento: la pagina di login viene ri-renderizzata
|
||||
const bool stillForm = respBody.contains("name=\"password\"")
|
||||
|| respBody.contains("name='password'");
|
||||
if (status >= 400 || stillForm) {
|
||||
qDebug() << "login fallito status:" << status
|
||||
<< "form-ancora-presente=" << stillForm;
|
||||
finishLogin(false, QStringLiteral(
|
||||
"Login fallito: credenziali non valide o accesso bloccato"));
|
||||
} else {
|
||||
qDebug() << "login OK status:" << status;
|
||||
finishLogin(true, QString());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void ApiClient::finishLogin(bool ok, const QString &message)
|
||||
{
|
||||
m_loginInProgress = false;
|
||||
m_loggedIn = ok;
|
||||
if (!ok)
|
||||
m_loginCooldownUntil = QDateTime::currentMSecsSinceEpoch() + 60000;
|
||||
const QList<std::function<void()>> queue = m_loginQueue;
|
||||
m_loginQueue.clear();
|
||||
if (!ok && !message.isEmpty())
|
||||
emit loginFailed(message);
|
||||
// esegui la coda anche in caso di fallimento: le richieste andranno a
|
||||
// vuoto in modo rilevato (bookmarkError), senza bloccare il lettore
|
||||
for (const auto &fn : queue)
|
||||
fn();
|
||||
}
|
||||
|
||||
void ApiClient::postBookmark(int bookId, const QString &format, const QString &key)
|
||||
{
|
||||
ensureLogin([this, bookId, format, key]() {
|
||||
sendBookmark(bookId, format, key);
|
||||
});
|
||||
}
|
||||
|
||||
void ApiClient::sendBookmark(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, bookId]() {
|
||||
reply->deleteLater();
|
||||
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
const QByteArray respBody = reply->readAll();
|
||||
qDebug() << "postBookmark status:" << status;
|
||||
// senza sessione il server risponde col form di login (302 seguito)
|
||||
const bool authFail = respBody.contains("name=\"password\"")
|
||||
|| respBody.contains("name='password'");
|
||||
if (authFail) {
|
||||
qDebug() << "postBookmark: sessione non valida (form di login nella risposta)";
|
||||
emit bookmarkError(bookId, QStringLiteral("Sync segnalibro: sessione non valida"));
|
||||
return;
|
||||
}
|
||||
// 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);
|
||||
} else if (status >= 400) {
|
||||
qDebug() << "postBookmark ERROR status:" << status;
|
||||
emit bookmarkError(bookId, QStringLiteral("Sync segnalibro fallita (%1)").arg(status));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 QString csrfToken = extractCsrfToken(reply->readAll());
|
||||
if (!csrfToken.isEmpty()) {
|
||||
postBookmarkWithCsrf(url, key, csrfToken);
|
||||
} 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::fetchBookmark(int bookId, const QString &format)
|
||||
{
|
||||
if (bookId <= 0)
|
||||
return;
|
||||
ensureLogin([this, bookId, format]() {
|
||||
requestBookmark(bookId, format);
|
||||
});
|
||||
}
|
||||
|
||||
void ApiClient::requestBookmark(int bookId, const QString &format)
|
||||
{
|
||||
const QString url = m_settings->baseUrl() + QStringLiteral("/read/")
|
||||
+ QString::number(bookId) + QLatin1Char('/') + format;
|
||||
qDebug() << "fetchBookmark:" << url;
|
||||
|
||||
QNetworkReply *reply = startGet(QUrl(url));
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, bookId, format]() {
|
||||
reply->deleteLater();
|
||||
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (reply->error() != QNetworkReply::NoError || status >= 400) {
|
||||
qDebug() << "fetchBookmark ERROR status:" << status
|
||||
<< "err:" << reply->errorString();
|
||||
emit bookmarkError(bookId, QStringLiteral(
|
||||
"Segnalibro server non disponibile (%1)").arg(status));
|
||||
return;
|
||||
}
|
||||
const QByteArray body = reply->readAll();
|
||||
// l'HTML del reader incorpora la posizione: bookmark: "chiave"
|
||||
QRegularExpression re(QStringLiteral("bookmark:\\s*\"([^\"]*)\""));
|
||||
const QRegularExpressionMatch m = re.match(QString::fromUtf8(body));
|
||||
const QString key = m.hasMatch() ? m.captured(1).trimmed() : QString();
|
||||
qDebug() << "fetchBookmark OK book:" << bookId << "key=[" << key << "]";
|
||||
emit bookmarkReady(bookId, format, key);
|
||||
});
|
||||
}
|
||||
|
||||
void ApiClient::fetchImage(const QString &url, const QString &id)
|
||||
{
|
||||
if (url.isEmpty()) {
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
#include <QObject>
|
||||
#include <QVariantList>
|
||||
|
||||
#include <functional>
|
||||
|
||||
class QNetworkReply;
|
||||
class QNetworkRequest;
|
||||
class Settings;
|
||||
@@ -14,9 +12,6 @@ class Settings;
|
||||
// Cliente OPDS: scarica i feed (getFeed), li parsa e li espone come QVariantList
|
||||
// di entry (title, authors, summary, coverUrl, formats, isNavigation, linkUrl).
|
||||
// Gestisce anche il fetch delle copertine in una cache locale su disco.
|
||||
// Le route web di calibre-web (login, /read, /ajax/bookmark) richiedono una
|
||||
// sessione flask-login: l'app fa login col form standard (username+password+
|
||||
// csrf_token) e riusa il cookie di sessione del QNetworkAccessManager.
|
||||
class ApiClient : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -26,44 +21,20 @@ 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);
|
||||
// legge il segnalibro dal server: GET /read/<id>/<format> e estrae
|
||||
// bookmark: "chiave" dall'HTML del reader
|
||||
Q_INVOKABLE void fetchBookmark(int bookId, const QString &format);
|
||||
|
||||
signals:
|
||||
void feedReady(const QVariantList &entries, const QString &nextUrl, const QString &feedTitle);
|
||||
void feedError(const QString &message);
|
||||
void imageReady(const QString &id, const QString &localPath);
|
||||
// chiave vuota = nessun segnalibro sul server
|
||||
void bookmarkReady(int bookId, const QString &format, const QString &key);
|
||||
void bookmarkError(int bookId, const QString &message);
|
||||
void loginFailed(const QString &message);
|
||||
|
||||
private:
|
||||
QNetworkReply *startGet(const QUrl &url);
|
||||
void applyAuth(QNetworkRequest &request);
|
||||
QString resolveUrl(const QString &relative) const;
|
||||
// login di sessione: esegue after() subito se già autenticati, altrimenti
|
||||
// fa login e poi esegue (coda per le richieste arrivate nel frattempo)
|
||||
void ensureLogin(const std::function<void()> &after);
|
||||
void finishLogin(bool ok, const QString &message);
|
||||
QString extractCsrfToken(const QByteArray &body) const;
|
||||
void sendBookmark(int bookId, const QString &format, const QString &key);
|
||||
void postBookmarkWithCsrf(const QString &url, const QString &key,
|
||||
const QString &csrfToken);
|
||||
void fetchCsrfTokenAndRetry(const QString &url, const QString &key);
|
||||
void requestBookmark(int bookId, const QString &format);
|
||||
|
||||
Settings *m_settings;
|
||||
QNetworkAccessManager m_nam;
|
||||
QString m_cacheDir;
|
||||
bool m_csrfRetried = false;
|
||||
bool m_loggedIn = false;
|
||||
bool m_loginInProgress = false;
|
||||
qint64 m_loginCooldownUntil = 0; // ms epoch: niente retry login per 60 s dopo un fallimento
|
||||
QList<std::function<void()>> m_loginQueue;
|
||||
};
|
||||
|
||||
#endif // APICLIENT_H
|
||||
|
||||
Reference in New Issue
Block a user