Files
harbour-calibreweb/src/apiclient.cpp
T

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());
}
});
}