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"