File indexing completed on 2024-04-28 15:29:01

0001 /*
0002     SPDX-FileCopyrightText: 2021 Wolthera van Hövell tot Westerflier <griffinvalley@gmail.com>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0005 */
0006 #include "opdsprovider_p.h"
0007 
0008 #include <KFormat>
0009 #include <KLocalizedString>
0010 #include <QDate>
0011 #include <QIcon>
0012 #include <QLocale>
0013 #include <QTimer>
0014 #include <QUrlQuery>
0015 #include <syndication/atom/atom.h>
0016 #include <syndication/documentsource.h>
0017 #include <syndication_export.h>
0018 
0019 #include <knewstuffcore_debug.h>
0020 
0021 #include "tagsfilterchecker.h"
0022 
0023 namespace KNSCore
0024 {
0025 static const QLatin1String OPDS_REL_ACQUISITION{"http://opds-spec.org/acquisition"};
0026 static const QLatin1String OPDS_REL_AC_OPEN_ACCESS{"http://opds-spec.org/acquisition/open-access"};
0027 static const QLatin1String OPDS_REL_AC_BORROW{"http://opds-spec.org/acquisition/borrow"};
0028 static const QLatin1String OPDS_REL_AC_BUY{"http://opds-spec.org/acquisition/buy"};
0029 static const QLatin1String OPDS_REL_AC_SUBSCRIBE{"http://opds-spec.org/acquisition/subscribe"};
0030 static const QLatin1String OPDS_REL_AC_SAMPLE{"http://opds-spec.org/acquisition/sample"};
0031 static const QLatin1String OPDS_REL_IMAGE{"http://opds-spec.org/image"};
0032 static const QLatin1String OPDS_REL_THUMBNAIL{"http://opds-spec.org/image/thumbnail"};
0033 static const QLatin1String OPDS_REL_CRAWL{"http://opds-spec.org/crawlable"};
0034 static const QLatin1String OPDS_REL_FACET{"http://opds-spec.org/facet"};
0035 static const QLatin1String OPDS_REL_SHELF{"http://opds-spec.org/shelf"};
0036 static const QLatin1String OPDS_REL_SORT_NEW{"http://opds-spec.org/sort/new"};
0037 static const QLatin1String OPDS_REL_SORT_POPULAR{"http://opds-spec.org/sort/popular"};
0038 static const QLatin1String OPDS_REL_FEATURED{"http://opds-spec.org/featured"};
0039 static const QLatin1String OPDS_REL_RECOMMENDED{"http://opds-spec.org/recommended"};
0040 static const QLatin1String OPDS_REL_SUBSCRIPTIONS{"http://opds-spec.org/subscriptions"};
0041 static const QLatin1String OPDS_EL_PRICE{"opds:price"};
0042 static const QLatin1String OPDS_EL_INDIRECT{"opds:indirectAcquisition"};
0043 static const QLatin1String OPDS_ATTR_FACET_GROUP{"opds:facetGroup"};
0044 static const QLatin1String OPDS_ATTR_ACTIVE_FACET{"opds:activeFacet"};
0045 
0046 static const QLatin1String OPDS_ATOM_MT{"application/atom+xml"};
0047 static const QLatin1String OPDS_PROFILE{"profile=opds-catalog"};
0048 static const QLatin1String OPDS_TYPE_ENTRY{"type=entry"};
0049 static const QLatin1String OPDS_KIND_NAVIGATION{"kind=navigation"};
0050 static const QLatin1String OPDS_KIND_ACQUISITION{"kind=acquisition"};
0051 
0052 static const QLatin1String REL_START{"start"};
0053 static const QLatin1String REL_SUBSECTION{"subsection"};
0054 static const QLatin1String REL_COLLECTION{"collection"};
0055 static const QLatin1String REL_PREVIEW{"preview"};
0056 static const QLatin1String REL_REPLIES{"replies"};
0057 static const QLatin1String REL_RELATED{"related"};
0058 static const QLatin1String REL_PREVIOUS{"previous"};
0059 static const QLatin1String REL_NEXT{"next"};
0060 static const QLatin1String REL_FIRST{"first"};
0061 static const QLatin1String REL_LAST{"last"};
0062 static const QLatin1String REL_UP{"up"};
0063 static const QLatin1String REL_SELF{"self"};
0064 static const QLatin1String REL_ALTERNATE{"alternate"};
0065 static const QLatin1String ATTR_CURRENCY_CODE{"currencycode"};
0066 static const QLatin1String FEED_COMPLETE{"fh:complete"};
0067 static const QLatin1String THREAD_COUNT{"count"};
0068 
0069 static const QLatin1String OPENSEARCH_NS{"http://a9.com/-/spec/opensearch/1.1/"};
0070 static const QLatin1String OPENSEARCH_MT{"application/opensearchdescription+xml"};
0071 static const QLatin1String REL_SEARCH{"search"};
0072 
0073 static const QLatin1String OPENSEARCH_EL_URL{"Url"};
0074 static const QLatin1String OPENSEARCH_ATTR_TYPE{"type"};
0075 static const QLatin1String OPENSEARCH_ATTR_TEMPLATE{"template"};
0076 static const QLatin1String OPENSEARCH_SEARCH_TERMS{"searchTerms"};
0077 static const QLatin1String OPENSEARCH_COUNT{"count"};
0078 static const QLatin1String OPENSEARCH_START_INDEX{"startIndex"};
0079 static const QLatin1String OPENSEARCH_START_PAGE{"startPage"};
0080 
0081 static const QLatin1String HTML_MT{"text/html"};
0082 
0083 static const QLatin1String KEY_MIME_TYPE{"data##mimetype="};
0084 static const QLatin1String KEY_URL{"data##url="};
0085 static const QLatin1String KEY_LANGUAGE{"data##language="};
0086 
0087 class OPDSProviderPrivate
0088 {
0089 public:
0090     OPDSProviderPrivate(OPDSProvider *qq)
0091         : q(qq)
0092         , initialized(false)
0093         , loadingExtraDetails(false)
0094     {
0095     }
0096     OPDSProvider *q;
0097     QString providerId;
0098     QString providerName;
0099     QUrl iconUrl;
0100     bool initialized;
0101 
0102     /***
0103      * OPDS catalogs consist of many small atom feeds. This variable
0104      * tracks which atom feed to load.
0105      */
0106     QUrl currentUrl;
0107     // partial url identifying the self. This is necessary to resolve relative links.
0108     QString selfUrl;
0109 
0110     QDateTime currentTime;
0111     bool loadingExtraDetails;
0112 
0113     XmlLoader *xmlLoader;
0114 
0115     EntryInternal::List cachedEntries;
0116     Provider::SearchRequest currentRequest;
0117 
0118     QUrl openSearchDocumentURL;
0119     QString openSearchTemplate;
0120 
0121     // Generate an opensearch string.
0122     QUrl openSearchStringForRequest(const KNSCore::Provider::SearchRequest &request)
0123     {
0124         {
0125             QUrl searchUrl = QUrl(openSearchTemplate);
0126 
0127             QUrlQuery templateQuery(searchUrl);
0128             QUrlQuery query;
0129 
0130             for (QPair<QString, QString> key : templateQuery.queryItems()) {
0131                 if (key.second.contains(OPENSEARCH_SEARCH_TERMS)) {
0132                     query.addQueryItem(key.first, request.searchTerm);
0133                 } else if (key.second.contains(OPENSEARCH_COUNT)) {
0134                     query.addQueryItem(key.first, QString::number(request.pageSize));
0135                 } else if (key.second.contains(OPENSEARCH_START_PAGE)) {
0136                     query.addQueryItem(key.first, QString::number(request.page));
0137                 } else if (key.second.contains(OPENSEARCH_START_INDEX)) {
0138                     query.addQueryItem(key.first, QString::number(request.page * request.pageSize));
0139                 }
0140             }
0141             searchUrl.setQuery(query);
0142             return searchUrl;
0143         }
0144     }
0145 
0146     // Handle URL resolving.
0147     QUrl fixRelativeUrl(QString urlPart)
0148     {
0149         QUrl query = QUrl(urlPart);
0150         if (query.isRelative()) {
0151             if (selfUrl.isEmpty() || QUrl(selfUrl).isRelative()) {
0152                 QUrl host = currentUrl;
0153                 return currentUrl.resolved(query);
0154             } else {
0155                 return QUrl(selfUrl).resolved(query);
0156             }
0157         }
0158         return query;
0159     };
0160 
0161     EntryInternal::List installedEntries() const {{EntryInternal::List entries;
0162     for (const EntryInternal &entry : std::as_const(cachedEntries)) {
0163         if (entry.status() == KNS3::Entry::Installed || entry.status() == KNS3::Entry::Updateable) {
0164             entries.append(entry);
0165         }
0166     }
0167     return entries;
0168 }
0169 };
0170 
0171 void slotLoadingFailed()
0172 {
0173     qCWarning(KNEWSTUFFCORE) << "OPDS Loading failed for" << currentUrl;
0174     Q_EMIT q->loadingFailed(currentRequest);
0175 };
0176 
0177 // Parse the opensearch configuration document.
0178 // https://github.com/dewitt/opensearch
0179 void parseOpenSearchDocument(const QDomDocument &doc){{openSearchTemplate = QString();
0180 if (doc.documentElement().attribute(QStringLiteral("xmlns")) != OPENSEARCH_NS) {
0181     qCWarning(KNEWSTUFFCORE) << "Opensearch link does not point at document with opensearch namespace" << openSearchDocumentURL;
0182     return;
0183 }
0184 QDomElement el = doc.documentElement().firstChildElement(OPENSEARCH_EL_URL);
0185 while (!el.isNull()) {
0186     if (el.attribute(OPENSEARCH_ATTR_TYPE).contains(OPDS_ATOM_MT)) {
0187         if (openSearchTemplate.isEmpty() || el.attribute(OPENSEARCH_ATTR_TYPE).contains(OPDS_PROFILE)) {
0188             openSearchTemplate = el.attribute(OPENSEARCH_ATTR_TEMPLATE);
0189         }
0190     }
0191 
0192     el = el.nextSiblingElement(OPENSEARCH_EL_URL);
0193 }
0194 }
0195 }
0196 ;
0197 
0198 /**
0199  * @brief parseFeedData
0200  * The main parsing function of this provider. Receives a QDomDocument
0201  * and parses that with Syndication's atom reader.
0202  * @param doc
0203  */
0204 void parseFeedData(const QDomDocument &doc)
0205 {
0206     Syndication::DocumentSource source(doc.toByteArray(), currentUrl.toString());
0207     Syndication::Atom::Parser parser;
0208     Syndication::Atom::FeedDocumentPtr feedDoc = parser.parse(source).staticCast<Syndication::Atom::FeedDocument>();
0209 
0210     QString fullEntryMimeType = QStringList({OPDS_ATOM_MT, OPDS_TYPE_ENTRY, OPDS_PROFILE}).join(QStringLiteral(";"));
0211 
0212     if (!feedDoc->isValid()) {
0213         qCWarning(KNEWSTUFFCORE) << "OPDS Feed at" << currentUrl << "not valid";
0214         Q_EMIT q->loadingFailed(currentRequest);
0215         return;
0216     }
0217     if (!feedDoc->title().isEmpty()) {
0218         providerName = feedDoc->title();
0219     }
0220     if (!feedDoc->icon().isEmpty()) {
0221         iconUrl = QUrl(fixRelativeUrl(feedDoc->icon()));
0222     }
0223 
0224     EntryInternal::List entries;
0225     QList<OPDSProvider::SearchPreset> presets;
0226 
0227     {
0228         OPDSProvider::SearchPreset preset;
0229         preset.providerId = providerId;
0230         OPDSProvider::SearchRequest request;
0231         request.searchTerm = providerId;
0232         preset.request = request;
0233         preset.type = Provider::SearchPresetTypes::Start;
0234         presets.append(preset);
0235     }
0236 
0237     // find the self link first!
0238     selfUrl.clear();
0239     for (auto link : feedDoc->links()) {
0240         if (link.rel().contains(REL_SELF)) {
0241             selfUrl = link.href();
0242         }
0243     }
0244 
0245     for (auto link : feedDoc->links()) {
0246         // There will be a number of links toplevel, amongst which probably a lot of sortorder and navigation links.
0247         if (link.rel() == REL_SEARCH && link.type() == OPENSEARCH_MT) {
0248             std::function<void(Syndication::Atom::Link)> osdUrlLoader;
0249             osdUrlLoader = [this, &osdUrlLoader](Syndication::Atom::Link theLink) {
0250                 openSearchDocumentURL = fixRelativeUrl(theLink.href());
0251                 xmlLoader = new XmlLoader(q);
0252 
0253                 QObject::connect(xmlLoader, &XmlLoader::signalLoaded, q, [this](const QDomDocument &doc) {
0254                     q->d->parseOpenSearchDocument(doc);
0255                 });
0256                 QObject::connect(xmlLoader, &XmlLoader::signalFailed, q, [this]() {
0257                     qCWarning(KNEWSTUFFCORE) << "OpenSearch XML Document Loading failed" << openSearchDocumentURL;
0258                 });
0259                 QObject::connect(
0260                     xmlLoader,
0261                     &XmlLoader::signalHttpError,
0262                     q,
0263                     [this, &osdUrlLoader, theLink](int status, QList<QNetworkReply::RawHeaderPair> rawHeaders) {
0264                         if (status == 503) { // Temporarily Unavailable
0265                             QDateTime retryAfter;
0266                             static const QByteArray retryAfterKey{"Retry-After"};
0267                             for (const QNetworkReply::RawHeaderPair &headerPair : rawHeaders) {
0268                                 if (headerPair.first == retryAfterKey) {
0269                                     // Retry-After is not a known header, so we need to do a bit of running around to make that work
0270                                     // Also, the fromHttpDate function is in the private qnetworkrequest header, so we can't use that
0271                                     // So, simple workaround, just pass it through a dummy request and get a formatted date out (the
0272                                     // cost is sufficiently low here, given we've just done a bunch of i/o heavy things, so...)
0273                                     QNetworkRequest dummyRequest;
0274                                     dummyRequest.setRawHeader(QByteArray{"Last-Modified"}, headerPair.second);
0275                                     retryAfter = dummyRequest.header(QNetworkRequest::LastModifiedHeader).toDateTime();
0276                                     break;
0277                                 }
0278                             }
0279                             QTimer::singleShot(retryAfter.toMSecsSinceEpoch() - QDateTime::currentMSecsSinceEpoch(), q, [&osdUrlLoader, theLink]() {
0280                                 osdUrlLoader(theLink);
0281                             });
0282                             // if it's a matter of a human moment's worth of seconds, just reload
0283                             if (retryAfter.toSecsSinceEpoch() - QDateTime::currentSecsSinceEpoch() > 2) {
0284                                 // more than that, spit out TryAgainLaterError to let the user know what we're doing with their time
0285                                 static const KFormat formatter;
0286                                 Q_EMIT q->signalErrorCode(
0287                                     KNSCore::TryAgainLaterError,
0288                                     i18n("The service is currently undergoing maintenance and is expected to be back in %1.",
0289                                          formatter.formatSpelloutDuration(retryAfter.toMSecsSinceEpoch() - QDateTime::currentMSecsSinceEpoch())),
0290                                     {retryAfter});
0291                             }
0292                         }
0293                     });
0294 
0295                 xmlLoader->load(openSearchDocumentURL);
0296             };
0297         } else if (link.type().contains(OPDS_PROFILE) && link.rel() != REL_SELF) {
0298             OPDSProvider::SearchPreset preset;
0299             preset.providerId = providerId;
0300             preset.displayName = link.title();
0301             OPDSProvider::SearchRequest request;
0302             request.searchTerm = fixRelativeUrl(link.href()).toString();
0303             preset.request = request;
0304             if (link.rel() == REL_START) {
0305                 preset.type = Provider::SearchPresetTypes::Root;
0306             } else if (link.rel() == OPDS_REL_FEATURED) {
0307                 preset.type = Provider::SearchPresetTypes::Featured;
0308             } else if (link.rel() == OPDS_REL_SHELF) {
0309                 preset.type = Provider::SearchPresetTypes::Shelf;
0310             } else if (link.rel() == OPDS_REL_SORT_NEW) {
0311                 preset.type = Provider::SearchPresetTypes::New;
0312             } else if (link.rel() == OPDS_REL_SORT_POPULAR) {
0313                 preset.type = Provider::SearchPresetTypes::Popular;
0314             } else if (link.rel() == REL_UP) {
0315                 preset.type = Provider::SearchPresetTypes::FolderUp;
0316             } else if (link.rel() == OPDS_REL_CRAWL) {
0317                 preset.type = Provider::SearchPresetTypes::AllEntries;
0318             } else if (link.rel() == OPDS_REL_RECOMMENDED) {
0319                 preset.type = Provider::SearchPresetTypes::Recommended;
0320             } else if (link.rel() == OPDS_REL_SUBSCRIPTIONS) {
0321                 preset.type = Provider::SearchPresetTypes::Subscription;
0322             } else {
0323                 preset.type = Provider::SearchPresetTypes::NoPresetType;
0324                 if (preset.displayName.isEmpty()) {
0325                     preset.displayName = link.rel();
0326                 }
0327             }
0328             presets.append(preset);
0329         }
0330     }
0331     TagsFilterChecker downloadTagChecker(q->downloadTagFilter());
0332     TagsFilterChecker entryTagChecker(q->tagFilter());
0333 
0334     for (int i = 0; i < feedDoc->entries().size(); i++) {
0335         Syndication::Atom::Entry feedEntry = feedDoc->entries().at(i);
0336 
0337         EntryInternal entry;
0338         entry.setName(feedEntry.title());
0339         entry.setProviderId(providerId);
0340         entry.setUniqueId(feedEntry.id());
0341 
0342         entry.setStatus(KNS3::Entry::Invalid);
0343         for (const EntryInternal &cachedEntry : std::as_const(cachedEntries)) {
0344             if (entry.uniqueId() == cachedEntry.uniqueId()) {
0345                 entry = cachedEntry;
0346                 break;
0347             }
0348         }
0349 
0350         // This is a bit of a pickle: atom feeds can have multiple categories.
0351         // but these categories are not specifically tags...
0352         QStringList entryTags;
0353         for (int j = 0; j < feedEntry.categories().size(); j++) {
0354             QString tag = feedEntry.categories().at(j).label();
0355             if (tag.isEmpty()) {
0356                 tag = feedEntry.categories().at(j).term();
0357             }
0358             entryTags.append(tag);
0359         }
0360         if (entryTagChecker.filterAccepts(entryTags)) {
0361             entry.setTags(entryTags);
0362         } else {
0363             continue;
0364         }
0365         // Same issue with author...
0366         for (int j = 0; j < feedEntry.authors().size(); j++) {
0367             Author author;
0368             Syndication::Atom::Person person = feedEntry.authors().at(j);
0369             author.setId(person.uri());
0370             author.setName(person.name());
0371             author.setEmail(person.email());
0372             author.setHomepage(person.uri());
0373             entry.setAuthor(author);
0374         }
0375         entry.setLicense(feedEntry.rights());
0376         if (feedEntry.content().isEscapedHTML()) {
0377             entry.setSummary(feedEntry.content().childNodesAsXML());
0378         } else {
0379             entry.setSummary(feedEntry.content().asString());
0380         }
0381         entry.setShortSummary(feedEntry.summary());
0382 
0383         int counterThumbnails = 0;
0384         int counterImages = 0;
0385         QString groupEntryUrl;
0386         for (int j = 0; j < feedEntry.links().size(); j++) {
0387             Syndication::Atom::Link link = feedEntry.links().at(j);
0388 
0389             KNSCore::EntryInternal::DownloadLinkInformation download;
0390             download.id = entry.downloadLinkCount() + 1;
0391             // Linkrelations can have multiple values, expressed as something like... rel="me nofollow alternate".
0392             QStringList linkRelation = link.rel().split(QStringLiteral(" "));
0393 
0394             QStringList tags;
0395             tags.append(KEY_MIME_TYPE + link.type());
0396             if (!link.hrefLanguage().isEmpty()) {
0397                 tags.append(KEY_LANGUAGE + link.hrefLanguage());
0398             }
0399             QString linkUrl = fixRelativeUrl(link.href()).toString();
0400             tags.append(KEY_URL + linkUrl);
0401             download.name = link.title();
0402             download.size = link.length() / 1000;
0403             download.tags = tags;
0404             download.isDownloadtypeLink = false;
0405 
0406             if (link.rel().startsWith(OPDS_REL_ACQUISITION)) {
0407                 if (link.title().isEmpty()) {
0408                     QStringList l;
0409                     l.append(link.type());
0410                     l.append(QStringLiteral("(") + link.rel().split(QStringLiteral("/")).last() + QStringLiteral(")"));
0411                     download.name = l.join(QStringLiteral(" "));
0412                 }
0413 
0414                 if (!downloadTagChecker.filterAccepts(download.tags)) {
0415                     continue;
0416                 }
0417 
0418                 if (linkRelation.contains(OPDS_REL_AC_BORROW) || linkRelation.contains(OPDS_REL_AC_SUBSCRIBE) || linkRelation.contains(OPDS_REL_AC_BUY)) {
0419                     // TODO we don't support borrow, buy and subscribe right now, requires authentication.
0420                     continue;
0421 
0422                 } else if (linkRelation.contains(OPDS_REL_ACQUISITION) || linkRelation.contains(OPDS_REL_AC_OPEN_ACCESS)) {
0423                     download.isDownloadtypeLink = true;
0424 
0425                     if (entry.status() != KNS3::Entry::Installed && entry.status() != KNS3::Entry::Updateable) {
0426                         entry.setStatus(KNS3::Entry::Downloadable);
0427                     }
0428 
0429                     entry.setEntryType(EntryInternal::CatalogEntry);
0430                 }
0431                 // TODO, support preview relation, but this requires we show that an entry is otherwise paid for in the UI.
0432 
0433                 for (QDomElement el : feedEntry.elementsByTagName(OPDS_EL_PRICE)) {
0434                     QLocale locale;
0435                     download.priceAmount = locale.toCurrencyString(el.text().toFloat(), el.attribute(ATTR_CURRENCY_CODE));
0436                 }
0437                 // There's an 'opds:indirectaquistition' element that gives extra metadata about bundles.
0438                 entry.appendDownloadLinkInformation(download);
0439 
0440             } else if (link.rel().startsWith(OPDS_REL_IMAGE)) {
0441                 if (link.rel() == OPDS_REL_THUMBNAIL) {
0442                     entry.setPreviewUrl(linkUrl, KNSCore::EntryInternal::PreviewType(counterThumbnails));
0443                     counterThumbnails += 1;
0444                 } else {
0445                     entry.setPreviewUrl(linkUrl, KNSCore::EntryInternal::PreviewType(counterImages + 3));
0446                     counterImages += 1;
0447                 }
0448 
0449             } else {
0450                 // This could be anything from a more info link, to navigation links, to links to the outside world.
0451                 // Todo: think of using link rel's 'replies', 'payment'(donation) and 'version-history'.
0452 
0453                 if (link.type().startsWith(OPDS_ATOM_MT)) {
0454                     if (link.type() == fullEntryMimeType) {
0455                         entry.appendDownloadLinkInformation(download);
0456                     } else {
0457                         groupEntryUrl = linkUrl;
0458                     }
0459 
0460                 } else if (link.type() == HTML_MT && linkRelation.contains(REL_ALTERNATE)) {
0461                     entry.setHomepage(QUrl(linkUrl));
0462 
0463                 } else if (downloadTagChecker.filterAccepts(download.tags)) {
0464                     entry.appendDownloadLinkInformation(download);
0465                 }
0466             }
0467         }
0468 
0469         // Todo:
0470         // feedEntry.elementsByTagName( dc:terms:issued ) is the official initial release date.
0471         // published is the released date of the opds catalog item, updated for the opds catalog item update.
0472         // maybe we should make sure to also check dc:terms:modified?
0473         // QDateTime date = QDateTime::fromSecsSinceEpoch(feedEntry.published());
0474 
0475         QDateTime date = QDateTime::fromSecsSinceEpoch(feedEntry.updated());
0476 
0477         if (entry.releaseDate().isNull()) {
0478             entry.setReleaseDate(date.date());
0479         }
0480 
0481         if (entry.status() != KNS3::Entry::Invalid) {
0482             entry.setPayload(QString());
0483             // Gutenberg doesn't do versioning in the opds, so it's update value is unreliable,
0484             // even though openlib and standard do use it properly. We'll instead doublecheck that
0485             // the new time is larger than 6min since we requested the feed.
0486             if (date.secsTo(currentTime) > 360) {
0487                 if (entry.releaseDate() < date.date()) {
0488                     entry.setUpdateReleaseDate(date.date());
0489                     if (entry.status() == KNS3::Entry::Installed) {
0490                         entry.setStatus(KNS3::Entry::Updateable);
0491                     }
0492                 }
0493             }
0494         }
0495         if (counterThumbnails == 0) {
0496             // fallback.
0497             if (!feedDoc->icon().isEmpty()) {
0498                 entry.setPreviewUrl(fixRelativeUrl(feedDoc->icon()).toString());
0499             }
0500         }
0501 
0502         if (entry.downloadLinkCount() == 0) {
0503             if (groupEntryUrl.isEmpty()) {
0504                 continue;
0505             } else {
0506                 entry.setEntryType(EntryInternal::GroupEntry);
0507                 entry.setPayload(groupEntryUrl);
0508             }
0509         }
0510 
0511         entries.append(entry);
0512     }
0513 
0514     if (loadingExtraDetails) {
0515         Q_EMIT q->entryDetailsLoaded(entries.first());
0516         loadingExtraDetails = false;
0517     } else {
0518         Q_EMIT q->loadingFinished(currentRequest, entries);
0519     }
0520     Q_EMIT q->searchPresetsLoaded(presets);
0521 };
0522 }
0523 ;
0524 
0525 OPDSProvider::OPDSProvider()
0526     : d(new OPDSProviderPrivate(this))
0527 {
0528 }
0529 
0530 OPDSProvider::~OPDSProvider() = default;
0531 
0532 QString OPDSProvider::id() const
0533 {
0534     return d->providerId;
0535 }
0536 
0537 QString OPDSProvider::name() const
0538 {
0539     return d->providerName;
0540 }
0541 
0542 QUrl OPDSProvider::icon() const
0543 {
0544     return d->iconUrl;
0545 }
0546 
0547 void OPDSProvider::loadEntries(const KNSCore::Provider::SearchRequest &request)
0548 {
0549     d->currentRequest = request;
0550 
0551     if (request.filter == Installed) {
0552         Q_EMIT loadingFinished(request, d->installedEntries());
0553         return;
0554     } else if (request.filter == Provider::ExactEntryId) {
0555         for (EntryInternal entry : d->cachedEntries) {
0556             if (entry.uniqueId() == request.searchTerm) {
0557                 loadEntryDetails(entry);
0558             }
0559         }
0560     } else {
0561         if (QUrl(request.searchTerm).scheme().startsWith(QStringLiteral("http"))) {
0562             d->currentUrl = QUrl(request.searchTerm);
0563         } else if (!d->openSearchTemplate.isEmpty() && !request.searchTerm.isEmpty()) {
0564             // We should check if there's an opensearch implementation, and see if we can funnel search
0565             // requests to that.
0566             d->currentUrl = d->openSearchStringForRequest(request);
0567         }
0568 
0569         // TODO request: check if entries is above pagesize*index, otherwise load next page.
0570 
0571         QUrl url = d->currentUrl;
0572         if (!url.isEmpty()) {
0573             qCDebug(KNEWSTUFFCORE) << "requesting url" << url;
0574             d->xmlLoader = new XmlLoader(this);
0575             d->currentTime = QDateTime::currentDateTime();
0576             d->loadingExtraDetails = false;
0577             connect(d->xmlLoader, &XmlLoader::signalLoaded, this, [this](const QDomDocument &doc) {
0578                 d->parseFeedData(doc);
0579             });
0580             connect(d->xmlLoader, &XmlLoader::signalFailed, this, [this]() {
0581                 d->slotLoadingFailed();
0582             });
0583             d->xmlLoader->load(url);
0584         } else {
0585             Q_EMIT loadingFailed(request);
0586         }
0587     }
0588 }
0589 
0590 void OPDSProvider::loadEntryDetails(const EntryInternal &entry)
0591 {
0592     QUrl url;
0593     QString entryMimeType = QStringList({OPDS_ATOM_MT, OPDS_TYPE_ENTRY, OPDS_PROFILE}).join(QStringLiteral(";"));
0594     for (auto link : entry.downloadLinkInformationList()) {
0595         if (link.tags.contains(KEY_MIME_TYPE + entryMimeType)) {
0596             for (QString string : link.tags) {
0597                 if (string.startsWith(KEY_URL)) {
0598                     url = QUrl(string.split(QStringLiteral("=")).last());
0599                 }
0600             }
0601         }
0602     }
0603     if (!url.isEmpty()) {
0604         d->xmlLoader = new XmlLoader(this);
0605         d->currentTime = QDateTime::currentDateTime();
0606         d->loadingExtraDetails = true;
0607         connect(d->xmlLoader, &XmlLoader::signalLoaded, this, [this](const QDomDocument &doc) {
0608             d->parseFeedData(doc);
0609         });
0610         connect(d->xmlLoader, &XmlLoader::signalFailed, this, [this]() {
0611             d->slotLoadingFailed();
0612         });
0613         d->xmlLoader->load(url);
0614     }
0615 }
0616 
0617 void OPDSProvider::loadPayloadLink(const KNSCore::EntryInternal &entry, int linkNumber)
0618 {
0619     KNSCore::EntryInternal copy = entry;
0620     for (auto downloadInfo : entry.downloadLinkInformationList()) {
0621         if (downloadInfo.id == linkNumber) {
0622             for (QString string : downloadInfo.tags) {
0623                 if (string.startsWith(KEY_URL)) {
0624                     copy.setPayload(string.split(QStringLiteral("=")).last());
0625                 }
0626             }
0627         }
0628     }
0629     Q_EMIT payloadLinkLoaded(copy);
0630 }
0631 
0632 bool OPDSProvider::setProviderXML(const QDomElement &xmldata)
0633 {
0634     if (xmldata.tagName() != QLatin1String("provider")) {
0635         return false;
0636     }
0637     d->providerId = xmldata.attribute(QStringLiteral("downloadurl"));
0638 
0639     QUrl iconurl(xmldata.attribute(QStringLiteral("icon")));
0640     if (!iconurl.isValid()) {
0641         iconurl = QUrl::fromLocalFile(xmldata.attribute(QStringLiteral("icon")));
0642     }
0643     d->iconUrl = iconurl;
0644 
0645     QDomNode n;
0646     for (n = xmldata.firstChild(); !n.isNull(); n = n.nextSibling()) {
0647         QDomElement e = n.toElement();
0648         if (e.tagName() == QLatin1String("title")) {
0649             d->providerName = e.text().trimmed();
0650         }
0651     }
0652 
0653     d->currentUrl = QUrl(d->providerId);
0654     QTimer::singleShot(0, this, [this]() {
0655         d->initialized = true;
0656         Q_EMIT providerInitialized(this);
0657     });
0658     return true;
0659 }
0660 
0661 bool OPDSProvider::isInitialized() const
0662 {
0663     return d->initialized;
0664 }
0665 
0666 void OPDSProvider::setCachedEntries(const KNSCore::EntryInternal::List &cachedEntries)
0667 {
0668     d->cachedEntries = cachedEntries;
0669 }
0670 }
0671 
0672 #include "moc_opdsprovider_p.cpp"