#include "apiclient.h" #include "opdsparser.h" #include "settings.h" #include #include #include #include #include #include #include #include #include #include #include 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 &)>( &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("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()); } }); }