File indexing completed on 2024-03-24 03:59: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"