File indexing completed on 2024-04-28 07:45:15

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