170 lines
6.2 KiB
C++
170 lines
6.2 KiB
C++
#include "apiclient.h"
|
|
|
|
#include "opdsparser.h"
|
|
#include "settings.h"
|
|
|
|
#include <QCryptographicHash>
|
|
#include <QDebug>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QNetworkReply>
|
|
#include <QNetworkRequest>
|
|
#include <QSslError>
|
|
#include <QStandardPaths>
|
|
#include <QTimer>
|
|
#include <QUrl>
|
|
|
|
ApiClient::ApiClient(Settings *settings, QObject *parent)
|
|
: QObject(parent)
|
|
, m_settings(settings)
|
|
{
|
|
m_cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
|
|
+ QStringLiteral("/covers");
|
|
QDir().mkpath(m_cacheDir);
|
|
}
|
|
|
|
QString ApiClient::resolveUrl(const QString &relative) const
|
|
{
|
|
if (relative.isEmpty())
|
|
return QString();
|
|
if (relative.startsWith(QLatin1String("http://"))
|
|
|| relative.startsWith(QLatin1String("https://")))
|
|
return relative;
|
|
// risoluzione RFC 3986: gestisce i path assoluti del server (es. /calibre/opds/books)
|
|
// senza raddoppiare il sottopath già presente nel baseUrl
|
|
QUrl base(m_settings->baseUrl());
|
|
return base.resolved(QUrl(relative)).toString();
|
|
}
|
|
|
|
void ApiClient::applyAuth(QNetworkRequest &request)
|
|
{
|
|
const QString auth = m_settings->authHeader();
|
|
if (!auth.isEmpty())
|
|
request.setRawHeader("Authorization", auth.toUtf8());
|
|
}
|
|
|
|
QNetworkReply *ApiClient::startGet(const QUrl &url)
|
|
{
|
|
QNetworkRequest request(url);
|
|
request.setRawHeader("Accept",
|
|
"application/atom+xml, application/xml, text/xml;q=0.9, */*;q=0.8");
|
|
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));
|
|
}
|
|
|
|
// timeout di sicurezza (30 s): senza, una richiesta verso un host irraggiungibile
|
|
// resta appesa all'infinito
|
|
QTimer *timer = new QTimer(reply);
|
|
timer->setSingleShot(true);
|
|
timer->setInterval(30000);
|
|
connect(timer, &QTimer::timeout, reply, &QNetworkReply::abort);
|
|
timer->start();
|
|
return reply;
|
|
}
|
|
|
|
void ApiClient::getFeed(const QString &url)
|
|
{
|
|
// i link dei feed OPDS sono spesso relativi (/opds/new): risolvi contro il server
|
|
const QString resolved = resolveUrl(url);
|
|
qDebug() << "getFeed request:" << resolved;
|
|
QNetworkReply *reply = startGet(QUrl(resolved));
|
|
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
|
reply->deleteLater();
|
|
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
|
if (reply->error() != QNetworkReply::NoError || status >= 400) {
|
|
QString message;
|
|
if (reply->error() == QNetworkReply::OperationCanceledError)
|
|
message = QStringLiteral("Timeout: il server non risponde (30 s)");
|
|
else if (status == 401)
|
|
message = QStringLiteral(
|
|
"Autenticazione richiesta (401). Controlla utente e password.");
|
|
else if (status == 404)
|
|
message = QStringLiteral("Endpoint non trovato (404). Controlla l'indirizzo.");
|
|
else
|
|
message = reply->errorString();
|
|
qDebug() << "getFeed ERROR status:" << status
|
|
<< "error:" << reply->error() << "-" << message;
|
|
emit feedError(message);
|
|
return;
|
|
}
|
|
|
|
const QByteArray body = reply->readAll();
|
|
QVariantList entries;
|
|
QString nextUrl;
|
|
QString feedTitle;
|
|
QString parseError;
|
|
if (OpdsParser::parse(body, &entries, &nextUrl, &feedTitle, &parseError)) {
|
|
qDebug() << "getFeed OK status:" << status
|
|
<< "entries:" << entries.size();
|
|
emit feedReady(entries, resolveUrl(nextUrl), feedTitle);
|
|
} else {
|
|
// risposta 2xx ma non XML: quasi sempre una pagina HTML (redirect/login/URL errato)
|
|
const QByteArray contentType = reply->header(
|
|
QNetworkRequest::ContentTypeHeader).toByteArray();
|
|
QString message;
|
|
if (contentType.contains("html") || body.trimmed().startsWith("<!DOCTYPE")
|
|
|| body.trimmed().startsWith("<html")) {
|
|
message = QStringLiteral(
|
|
"Il server ha risposto con una pagina HTML, non OPDS. "
|
|
"Controlla l'indirizzo (es. sottopath /calibre) e che il browse anonimo "
|
|
"o le credenziali siano corretti.");
|
|
} else {
|
|
message = parseError.isEmpty()
|
|
? QStringLiteral("Risposta non valida dal server")
|
|
: parseError;
|
|
}
|
|
qDebug() << "getFeed parse error:" << parseError
|
|
<< "content-type:" << contentType;
|
|
emit feedError(message);
|
|
}
|
|
});
|
|
}
|
|
|
|
void ApiClient::fetchImage(const QString &url, const QString &id)
|
|
{
|
|
if (url.isEmpty()) {
|
|
emit imageReady(id, QString());
|
|
return;
|
|
}
|
|
|
|
const QString resolved = resolveUrl(url);
|
|
const QUrl imageUrl(resolved);
|
|
|
|
QString fileName = QString::fromLatin1(
|
|
QCryptographicHash::hash(imageUrl.path().toUtf8(), QCryptographicHash::Md5).toHex());
|
|
const QString suffix = QFileInfo(imageUrl.path()).suffix();
|
|
if (!suffix.isEmpty() && suffix.length() <= 5)
|
|
fileName += QLatin1Char('.') + suffix;
|
|
const QString localPath = m_cacheDir + QLatin1Char('/') + fileName;
|
|
|
|
if (QFile::exists(localPath)) {
|
|
emit imageReady(id, localPath);
|
|
return;
|
|
}
|
|
|
|
QNetworkReply *reply = startGet(imageUrl);
|
|
connect(reply, &QNetworkReply::finished, this, [this, reply, id, localPath]() {
|
|
reply->deleteLater();
|
|
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
|
if (reply->error() != QNetworkReply::NoError || status >= 400) {
|
|
emit imageReady(id, QString());
|
|
return;
|
|
}
|
|
QFile file(localPath);
|
|
if (file.open(QIODevice::WriteOnly)) {
|
|
file.write(reply->readAll());
|
|
file.close();
|
|
emit imageReady(id, localPath);
|
|
} else {
|
|
emit imageReady(id, QString());
|
|
}
|
|
});
|
|
}
|