Client OPDS per Calibre Web (Sailfish OS): core C++ testato, UI Silica, packaging RPM

This commit is contained in:
2026-08-12 01:45:31 +02:00
commit 85b4f8e9b9
25 changed files with 1518 additions and 0 deletions
+135
View File
@@ -0,0 +1,135 @@
#include "apiclient.h"
#include "opdsparser.h"
#include "settings.h"
#include <QCryptographicHash>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSslError>
#include <QStandardPaths>
#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;
const QString base = m_settings->baseUrl();
if (relative.startsWith(QLatin1Char('/')))
return base + relative;
return base + QLatin1Char('/') + relative;
}
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));
}
return reply;
}
void ApiClient::getFeed(const QString &url)
{
QNetworkReply *reply = startGet(QUrl(url));
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 (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();
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)) {
emit feedReady(entries, resolveUrl(nextUrl), feedTitle);
} else {
emit feedError(parseError.isEmpty()
? QStringLiteral("Risposta non valida dal server")
: parseError);
}
});
}
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());
}
});
}