1/*
2 SPDX-FileCopyrightText: 2021 Wolthera van Hövell tot Westerflier <griffinvalley@gmail.com>
3
4 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5*/
6#include "opdsprovider_p.h"
7
8#include <KFormat>
9#include <KLocalizedString>
10#include <QDate>
11#include <QIcon>
12#include <QLocale>
13#include <QTimer>
14#include <QUrlQuery>
15#include <syndication/atom/atom.h>
16#include <syndication/documentsource.h>
17
18#include <knewstuffcore_debug.h>
19
20#include "tagsfilterchecker.h"
21
22namespace KNSCore
23{
24static const QLatin1String OPDS_REL_ACQUISITION{"http://opds-spec.org/acquisition"};
25static const QLatin1String OPDS_REL_AC_OPEN_ACCESS{"http://opds-spec.org/acquisition/open-access"};
26static const QLatin1String OPDS_REL_AC_BORROW{"http://opds-spec.org/acquisition/borrow"};
27static const QLatin1String OPDS_REL_AC_BUY{"http://opds-spec.org/acquisition/buy"};
28static const QLatin1String OPDS_REL_AC_SUBSCRIBE{"http://opds-spec.org/acquisition/subscribe"};
29// static const QLatin1String OPDS_REL_AC_SAMPLE{"http://opds-spec.org/acquisition/sample"};
30static const QLatin1String OPDS_REL_IMAGE{"http://opds-spec.org/image"};
31static const QLatin1String OPDS_REL_THUMBNAIL{"http://opds-spec.org/image/thumbnail"};
32static const QLatin1String OPDS_REL_CRAWL{"http://opds-spec.org/crawlable"};
33// static const QLatin1String OPDS_REL_FACET{"http://opds-spec.org/facet"};
34static const QLatin1String OPDS_REL_SHELF{"http://opds-spec.org/shelf"};
35static const QLatin1String OPDS_REL_SORT_NEW{"http://opds-spec.org/sort/new"};
36static const QLatin1String OPDS_REL_SORT_POPULAR{"http://opds-spec.org/sort/popular"};
37static const QLatin1String OPDS_REL_FEATURED{"http://opds-spec.org/featured"};
38static const QLatin1String OPDS_REL_RECOMMENDED{"http://opds-spec.org/recommended"};
39static const QLatin1String OPDS_REL_SUBSCRIPTIONS{"http://opds-spec.org/subscriptions"};
40static const QLatin1String OPDS_EL_PRICE{"opds:price"};
41// static const QLatin1String OPDS_EL_INDIRECT{"opds:indirectAcquisition"};
42// static const QLatin1String OPDS_ATTR_FACET_GROUP{"opds:facetGroup"};
43// static const QLatin1String OPDS_ATTR_ACTIVE_FACET{"opds:activeFacet"};
44
45static const QLatin1String OPDS_ATOM_MT{"application/atom+xml"};
46static const QLatin1String OPDS_PROFILE{"profile=opds-catalog"};
47static const QLatin1String OPDS_TYPE_ENTRY{"type=entry"};
48// static const QLatin1String OPDS_KIND_NAVIGATION{"kind=navigation"};
49// static const QLatin1String OPDS_KIND_ACQUISITION{"kind=acquisition"};
50
51static const QLatin1String REL_START{"start"};
52// static const QLatin1String REL_SUBSECTION{"subsection"};
53// static const QLatin1String REL_COLLECTION{"collection"};
54// static const QLatin1String REL_PREVIEW{"preview"};
55// static const QLatin1String REL_REPLIES{"replies"};
56// static const QLatin1String REL_RELATED{"related"};
57// static const QLatin1String REL_PREVIOUS{"previous"};
58// static const QLatin1String REL_NEXT{"next"};
59// static const QLatin1String REL_FIRST{"first"};
60// static const QLatin1String REL_LAST{"last"};
61static const QLatin1String REL_UP{"up"};
62static const QLatin1String REL_SELF{"self"};
63static const QLatin1String REL_ALTERNATE{"alternate"};
64static const QLatin1String ATTR_CURRENCY_CODE{"currencycode"};
65// static const QLatin1String FEED_COMPLETE{"fh:complete"};
66// static const QLatin1String THREAD_COUNT{"count"};
67
68static const QLatin1String OPENSEARCH_NS{"http://a9.com/-/spec/opensearch/1.1/"};
69static const QLatin1String OPENSEARCH_MT{"application/opensearchdescription+xml"};
70static const QLatin1String REL_SEARCH{"search"};
71
72static const QLatin1String OPENSEARCH_EL_URL{"Url"};
73static const QLatin1String OPENSEARCH_ATTR_TYPE{"type"};
74static const QLatin1String OPENSEARCH_ATTR_TEMPLATE{"template"};
75static const QLatin1String OPENSEARCH_SEARCH_TERMS{"searchTerms"};
76static const QLatin1String OPENSEARCH_COUNT{"count"};
77static const QLatin1String OPENSEARCH_START_INDEX{"startIndex"};
78static const QLatin1String OPENSEARCH_START_PAGE{"startPage"};
79
80static const QLatin1String HTML_MT{"text/html"};
81
82static const QLatin1String KEY_MIME_TYPE{"data##mimetype="};
83static const QLatin1String KEY_URL{"data##url="};
84static const QLatin1String KEY_LANGUAGE{"data##language="};
85
86class OPDSProviderPrivate
87{
88public:
89 OPDSProviderPrivate(OPDSProvider *qq)
90 : q(qq)
91 , initialized(false)
92 , loadingExtraDetails(false)
93 {
94 }
95 OPDSProvider *q;
96 QString providerId;
97 QString providerName;
98 QUrl iconUrl;
99 bool initialized;
100
101 /***
102 * OPDS catalogs consist of many small atom feeds. This variable
103 * tracks which atom feed to load.
104 */
105 QUrl currentUrl;
106 // partial url identifying the self. This is necessary to resolve relative links.
107 QString selfUrl;
108
109 QDateTime currentTime;
110 bool loadingExtraDetails;
111
112 XmlLoader *xmlLoader;
113
114 Entry::List cachedEntries;
115 Provider::SearchRequest currentRequest;
116
117 QUrl openSearchDocumentURL;
118 QString openSearchTemplate;
119
120 // Generate an opensearch string.
121 QUrl openSearchStringForRequest(const KNSCore::Provider::SearchRequest &request)
122 {
123 {
124 QUrl searchUrl = QUrl(openSearchTemplate);
125
126 QUrlQuery templateQuery(searchUrl);
127 QUrlQuery query;
128
129 for (QPair<QString, QString> key : templateQuery.queryItems()) {
130 if (key.second.contains(s: OPENSEARCH_SEARCH_TERMS)) {
131 query.addQueryItem(key: key.first, value: request.searchTerm);
132 } else if (key.second.contains(s: OPENSEARCH_COUNT)) {
133 query.addQueryItem(key: key.first, value: QString::number(request.pageSize));
134 } else if (key.second.contains(s: OPENSEARCH_START_PAGE)) {
135 query.addQueryItem(key: key.first, value: QString::number(request.page));
136 } else if (key.second.contains(s: OPENSEARCH_START_INDEX)) {
137 query.addQueryItem(key: key.first, value: QString::number(request.page * request.pageSize));
138 }
139 }
140 searchUrl.setQuery(query);
141 return searchUrl;
142 }
143 }
144
145 // Handle URL resolving.
146 QUrl fixRelativeUrl(QString urlPart)
147 {
148 QUrl query = QUrl(urlPart);
149 if (query.isRelative()) {
150 if (selfUrl.isEmpty() || QUrl(selfUrl).isRelative()) {
151 return currentUrl.resolved(relative: query);
152 } else {
153 return QUrl(selfUrl).resolved(relative: query);
154 }
155 }
156 return query;
157 };
158
159 Entry::List installedEntries() const {{Entry::List entries;
160 for (const Entry &entry : std::as_const(t: cachedEntries)) {
161 if (entry.status() == KNSCore::Entry::Installed || entry.status() == KNSCore::Entry::Updateable) {
162 entries.append(t: entry);
163 }
164 }
165 return entries;
166}
167};
168
169void slotLoadingFailed()
170{
171 qCWarning(KNEWSTUFFCORE) << "OPDS Loading failed for" << currentUrl;
172 Q_EMIT q->loadingFailed(currentRequest);
173};
174
175// Parse the opensearch configuration document.
176// https://github.com/dewitt/opensearch
177void parseOpenSearchDocument(const QDomDocument &doc){{openSearchTemplate = QString();
178if (doc.documentElement().attribute(QStringLiteral("xmlns")) != OPENSEARCH_NS) {
179 qCWarning(KNEWSTUFFCORE) << "Opensearch link does not point at document with opensearch namespace" << openSearchDocumentURL;
180 return;
181}
182QDomElement el = doc.documentElement().firstChildElement(tagName: OPENSEARCH_EL_URL);
183while (!el.isNull()) {
184 if (el.attribute(name: OPENSEARCH_ATTR_TYPE).contains(s: OPDS_ATOM_MT)) {
185 if (openSearchTemplate.isEmpty() || el.attribute(name: OPENSEARCH_ATTR_TYPE).contains(s: OPDS_PROFILE)) {
186 openSearchTemplate = el.attribute(name: OPENSEARCH_ATTR_TEMPLATE);
187 }
188 }
189
190 el = el.nextSiblingElement(taName: OPENSEARCH_EL_URL);
191}
192}
193}
194;
195
196/**
197 * @brief parseFeedData
198 * The main parsing function of this provider. Receives a QDomDocument
199 * and parses that with Syndication's atom reader.
200 * @param doc
201 */
202void parseFeedData(const QDomDocument &doc)
203{
204 Syndication::DocumentSource source(doc.toByteArray(), currentUrl.toString());
205 Syndication::Atom::Parser parser;
206 Syndication::Atom::FeedDocumentPtr feedDoc = parser.parse(source).staticCast<Syndication::Atom::FeedDocument>();
207
208 QString fullEntryMimeType = QStringList({OPDS_ATOM_MT, OPDS_TYPE_ENTRY, OPDS_PROFILE}).join(QStringLiteral(";"));
209
210 if (!feedDoc->isValid()) {
211 qCWarning(KNEWSTUFFCORE) << "OPDS Feed at" << currentUrl << "not valid";
212 Q_EMIT q->loadingFailed(currentRequest);
213 return;
214 }
215 if (!feedDoc->title().isEmpty()) {
216 providerName = feedDoc->title();
217 }
218 if (!feedDoc->icon().isEmpty()) {
219 iconUrl = QUrl(fixRelativeUrl(urlPart: feedDoc->icon()));
220 }
221
222 Entry::List entries;
223 QList<OPDSProvider::SearchPreset> presets;
224
225 {
226 OPDSProvider::SearchPreset preset;
227 preset.providerId = providerId;
228 OPDSProvider::SearchRequest request;
229 request.searchTerm = providerId;
230 preset.request = request;
231 preset.type = Provider::SearchPresetTypes::Start;
232 presets.append(t: preset);
233 }
234
235 // find the self link first!
236 selfUrl.clear();
237 for (auto link : feedDoc->links()) {
238 if (link.rel().contains(s: REL_SELF)) {
239 selfUrl = link.href();
240 }
241 }
242
243 for (auto link : feedDoc->links()) {
244 // There will be a number of links toplevel, amongst which probably a lot of sortorder and navigation links.
245 if (link.rel() == REL_SEARCH && link.type() == OPENSEARCH_MT) {
246 std::function<void(Syndication::Atom::Link)> osdUrlLoader;
247 osdUrlLoader = [this, &osdUrlLoader](Syndication::Atom::Link theLink) {
248 openSearchDocumentURL = fixRelativeUrl(urlPart: theLink.href());
249 xmlLoader = new XmlLoader(q);
250
251 QObject::connect(sender: xmlLoader, signal: &XmlLoader::signalLoaded, context: q, slot: [this](const QDomDocument &doc) {
252 q->d->parseOpenSearchDocument(doc);
253 });
254 QObject::connect(sender: xmlLoader, signal: &XmlLoader::signalFailed, context: q, slot: [this]() {
255 qCWarning(KNEWSTUFFCORE) << "OpenSearch XML Document Loading failed" << openSearchDocumentURL;
256 });
257 QObject::connect(
258 sender: xmlLoader,
259 signal: &XmlLoader::signalHttpError,
260 context: q,
261 slot: [this, &osdUrlLoader, theLink](int status, QList<QNetworkReply::RawHeaderPair> rawHeaders) { // clazy:exclude=lambda-in-connect
262 if (status == 503) { // Temporarily Unavailable
263 QDateTime retryAfter;
264 static const QByteArray retryAfterKey{"Retry-After"};
265 for (const QNetworkReply::RawHeaderPair &headerPair : rawHeaders) {
266 if (headerPair.first == retryAfterKey) {
267 // Retry-After is not a known header, so we need to do a bit of running around to make that work
268 // Also, the fromHttpDate function is in the private qnetworkrequest header, so we can't use that
269 // So, simple workaround, just pass it through a dummy request and get a formatted date out (the
270 // cost is sufficiently low here, given we've just done a bunch of i/o heavy things, so...)
271 QNetworkRequest dummyRequest;
272 dummyRequest.setRawHeader(headerName: QByteArray{"Last-Modified"}, value: headerPair.second);
273 retryAfter = dummyRequest.header(header: QNetworkRequest::LastModifiedHeader).toDateTime();
274 break;
275 }
276 }
277 // clazy:exclude=lambda-in-connect
278 QTimer::singleShot(interval: retryAfter.toMSecsSinceEpoch() - QDateTime::currentMSecsSinceEpoch(), receiver: q, slot: [&osdUrlLoader, theLink]() {
279 osdUrlLoader(theLink);
280 });
281 // if it's a matter of a human moment's worth of seconds, just reload
282 if (retryAfter.toSecsSinceEpoch() - QDateTime::currentSecsSinceEpoch() > 2) {
283 // more than that, spit out TryAgainLaterError to let the user know what we're doing with their time
284 static const KFormat formatter;
285 Q_EMIT q->signalErrorCode(
286 errorCode: KNSCore::ErrorCode::TryAgainLaterError,
287 i18n("The service is currently undergoing maintenance and is expected to be back in %1.",
288 formatter.formatSpelloutDuration(retryAfter.toMSecsSinceEpoch() - QDateTime::currentMSecsSinceEpoch())),
289 metadata: {retryAfter});
290 }
291 }
292 });
293
294 xmlLoader->load(url: openSearchDocumentURL);
295 };
296 } else if (link.type().contains(s: OPDS_PROFILE) && link.rel() != REL_SELF) {
297 OPDSProvider::SearchPreset preset;
298 preset.providerId = providerId;
299 preset.displayName = link.title();
300 OPDSProvider::SearchRequest request;
301 request.searchTerm = fixRelativeUrl(urlPart: link.href()).toString();
302 preset.request = request;
303 if (link.rel() == REL_START) {
304 preset.type = Provider::SearchPresetTypes::Root;
305 } else if (link.rel() == OPDS_REL_FEATURED) {
306 preset.type = Provider::SearchPresetTypes::Featured;
307 } else if (link.rel() == OPDS_REL_SHELF) {
308 preset.type = Provider::SearchPresetTypes::Shelf;
309 } else if (link.rel() == OPDS_REL_SORT_NEW) {
310 preset.type = Provider::SearchPresetTypes::New;
311 } else if (link.rel() == OPDS_REL_SORT_POPULAR) {
312 preset.type = Provider::SearchPresetTypes::Popular;
313 } else if (link.rel() == REL_UP) {
314 preset.type = Provider::SearchPresetTypes::FolderUp;
315 } else if (link.rel() == OPDS_REL_CRAWL) {
316 preset.type = Provider::SearchPresetTypes::AllEntries;
317 } else if (link.rel() == OPDS_REL_RECOMMENDED) {
318 preset.type = Provider::SearchPresetTypes::Recommended;
319 } else if (link.rel() == OPDS_REL_SUBSCRIPTIONS) {
320 preset.type = Provider::SearchPresetTypes::Subscription;
321 } else {
322 preset.type = Provider::SearchPresetTypes::NoPresetType;
323 if (preset.displayName.isEmpty()) {
324 preset.displayName = link.rel();
325 }
326 }
327 presets.append(t: preset);
328 }
329 }
330 TagsFilterChecker downloadTagChecker(q->downloadTagFilter());
331 TagsFilterChecker entryTagChecker(q->tagFilter());
332
333 for (int i = 0; i < feedDoc->entries().size(); i++) {
334 Syndication::Atom::Entry feedEntry = feedDoc->entries().at(i);
335
336 Entry entry;
337 entry.setName(feedEntry.title());
338 entry.setProviderId(providerId);
339 entry.setUniqueId(feedEntry.id());
340
341 entry.setStatus(KNSCore::Entry::Invalid);
342 for (const Entry &cachedEntry : std::as_const(t&: cachedEntries)) {
343 if (entry.uniqueId() == cachedEntry.uniqueId()) {
344 entry = cachedEntry;
345 break;
346 }
347 }
348
349 // This is a bit of a pickle: atom feeds can have multiple categories.
350 // but these categories are not specifically tags...
351 QStringList entryTags;
352 for (int j = 0; j < feedEntry.categories().size(); j++) {
353 QString tag = feedEntry.categories().at(i: j).label();
354 if (tag.isEmpty()) {
355 tag = feedEntry.categories().at(i: j).term();
356 }
357 entryTags.append(t: tag);
358 }
359 if (entryTagChecker.filterAccepts(tags: entryTags)) {
360 entry.setTags(entryTags);
361 } else {
362 continue;
363 }
364 // Same issue with author...
365 for (int j = 0; j < feedEntry.authors().size(); j++) {
366 Author author;
367 Syndication::Atom::Person person = feedEntry.authors().at(i: j);
368 author.setId(person.uri());
369 author.setName(person.name());
370 author.setEmail(person.email());
371 author.setHomepage(person.uri());
372 entry.setAuthor(author);
373 }
374 entry.setLicense(feedEntry.rights());
375 if (feedEntry.content().isEscapedHTML()) {
376 entry.setSummary(feedEntry.content().childNodesAsXML());
377 } else {
378 entry.setSummary(feedEntry.content().asString());
379 }
380 entry.setShortSummary(feedEntry.summary());
381
382 int counterThumbnails = 0;
383 int counterImages = 0;
384 QString groupEntryUrl;
385 for (int j = 0; j < feedEntry.links().size(); j++) {
386 Syndication::Atom::Link link = feedEntry.links().at(i: j);
387
388 KNSCore::Entry::DownloadLinkInformation download;
389 download.id = entry.downloadLinkCount() + 1;
390 // Linkrelations can have multiple values, expressed as something like... rel="me nofollow alternate".
391 QStringList linkRelation = link.rel().split(QStringLiteral(" "));
392
393 QStringList tags;
394 tags.append(t: KEY_MIME_TYPE + link.type());
395 if (!link.hrefLanguage().isEmpty()) {
396 tags.append(t: KEY_LANGUAGE + link.hrefLanguage());
397 }
398 QString linkUrl = fixRelativeUrl(urlPart: link.href()).toString();
399 tags.append(t: KEY_URL + linkUrl);
400 download.name = link.title();
401 download.size = link.length() / 1000;
402 download.tags = tags;
403 download.isDownloadtypeLink = false;
404
405 if (link.rel().startsWith(s: OPDS_REL_ACQUISITION)) {
406 if (link.title().isEmpty()) {
407 QStringList l;
408 l.append(t: link.type());
409 l.append(QStringLiteral("(") + link.rel().split(QStringLiteral("/")).last() + QStringLiteral(")"));
410 download.name = l.join(QStringLiteral(" "));
411 }
412
413 if (!downloadTagChecker.filterAccepts(tags: download.tags)) {
414 continue;
415 }
416
417 if (linkRelation.contains(str: OPDS_REL_AC_BORROW) || linkRelation.contains(str: OPDS_REL_AC_SUBSCRIBE) || linkRelation.contains(str: OPDS_REL_AC_BUY)) {
418 // TODO we don't support borrow, buy and subscribe right now, requires authentication.
419 continue;
420
421 } else if (linkRelation.contains(str: OPDS_REL_ACQUISITION) || linkRelation.contains(str: OPDS_REL_AC_OPEN_ACCESS)) {
422 download.isDownloadtypeLink = true;
423
424 if (entry.status() != KNSCore::Entry::Installed && entry.status() != KNSCore::Entry::Updateable) {
425 entry.setStatus(KNSCore::Entry::Downloadable);
426 }
427
428 entry.setEntryType(Entry::CatalogEntry);
429 }
430 // TODO, support preview relation, but this requires we show that an entry is otherwise paid for in the UI.
431
432 for (QDomElement el : feedEntry.elementsByTagName(tagName: OPDS_EL_PRICE)) {
433 QLocale locale;
434 download.priceAmount = locale.toCurrencyString(i: el.text().toFloat(), symbol: el.attribute(name: ATTR_CURRENCY_CODE));
435 }
436 // There's an 'opds:indirectaquistition' element that gives extra metadata about bundles.
437 entry.appendDownloadLinkInformation(info: download);
438
439 } else if (link.rel().startsWith(s: OPDS_REL_IMAGE)) {
440 if (link.rel() == OPDS_REL_THUMBNAIL) {
441 entry.setPreviewUrl(url: linkUrl, type: KNSCore::Entry::PreviewType(counterThumbnails));
442 counterThumbnails += 1;
443 } else {
444 entry.setPreviewUrl(url: linkUrl, type: KNSCore::Entry::PreviewType(counterImages + 3));
445 counterImages += 1;
446 }
447
448 } else {
449 // This could be anything from a more info link, to navigation links, to links to the outside world.
450 // Todo: think of using link rel's 'replies', 'payment'(donation) and 'version-history'.
451
452 if (link.type().startsWith(s: OPDS_ATOM_MT)) {
453 if (link.type() == fullEntryMimeType) {
454 entry.appendDownloadLinkInformation(info: download);
455 } else {
456 groupEntryUrl = linkUrl;
457 }
458
459 } else if (link.type() == HTML_MT && linkRelation.contains(str: REL_ALTERNATE)) {
460 entry.setHomepage(QUrl(linkUrl));
461
462 } else if (downloadTagChecker.filterAccepts(tags: download.tags)) {
463 entry.appendDownloadLinkInformation(info: download);
464 }
465 }
466 }
467
468 // Todo:
469 // feedEntry.elementsByTagName( dc:terms:issued ) is the official initial release date.
470 // published is the released date of the opds catalog item, updated for the opds catalog item update.
471 // maybe we should make sure to also check dc:terms:modified?
472 // QDateTime date = QDateTime::fromSecsSinceEpoch(feedEntry.published());
473
474 QDateTime date = QDateTime::fromSecsSinceEpoch(secs: feedEntry.updated());
475
476 if (entry.releaseDate().isNull()) {
477 entry.setReleaseDate(date.date());
478 }
479
480 if (entry.status() != KNSCore::Entry::Invalid) {
481 entry.setPayload(QString());
482 // Gutenberg doesn't do versioning in the opds, so it's update value is unreliable,
483 // even though openlib and standard do use it properly. We'll instead doublecheck that
484 // the new time is larger than 6min since we requested the feed.
485 if (date.secsTo(currentTime) > 360) {
486 if (entry.releaseDate() < date.date()) {
487 entry.setUpdateReleaseDate(date.date());
488 if (entry.status() == KNSCore::Entry::Installed) {
489 entry.setStatus(KNSCore::Entry::Updateable);
490 }
491 }
492 }
493 }
494 if (counterThumbnails == 0) {
495 // fallback.
496 if (!feedDoc->icon().isEmpty()) {
497 entry.setPreviewUrl(url: fixRelativeUrl(urlPart: feedDoc->icon()).toString());
498 }
499 }
500
501 if (entry.downloadLinkCount() == 0) {
502 if (groupEntryUrl.isEmpty()) {
503 continue;
504 } else {
505 entry.setEntryType(Entry::GroupEntry);
506 entry.setPayload(groupEntryUrl);
507 }
508 }
509
510 entries.append(t: entry);
511 }
512
513 if (loadingExtraDetails) {
514 Q_EMIT q->entryDetailsLoaded(entries.first());
515 loadingExtraDetails = false;
516 } else {
517 Q_EMIT q->loadingFinished(currentRequest, entries);
518 }
519 Q_EMIT q->searchPresetsLoaded(presets);
520};
521}
522;
523
524OPDSProvider::OPDSProvider()
525 : d(new OPDSProviderPrivate(this))
526{
527}
528
529OPDSProvider::~OPDSProvider() = default;
530
531QString OPDSProvider::id() const
532{
533 return d->providerId;
534}
535
536QString OPDSProvider::name() const
537{
538 return d->providerName;
539}
540
541QUrl OPDSProvider::icon() const
542{
543 return d->iconUrl;
544}
545
546void OPDSProvider::loadEntries(const KNSCore::Provider::SearchRequest &request)
547{
548 d->currentRequest = request;
549
550 if (request.filter == Installed) {
551 Q_EMIT loadingFinished(request, d->installedEntries());
552 return;
553 } else if (request.filter == Provider::ExactEntryId) {
554 for (Entry entry : d->cachedEntries) {
555 if (entry.uniqueId() == request.searchTerm) {
556 loadEntryDetails(entry);
557 }
558 }
559 } else {
560 if (QUrl(request.searchTerm).scheme().startsWith(QStringLiteral("http"))) {
561 d->currentUrl = QUrl(request.searchTerm);
562 } else if (!d->openSearchTemplate.isEmpty() && !request.searchTerm.isEmpty()) {
563 // We should check if there's an opensearch implementation, and see if we can funnel search
564 // requests to that.
565 d->currentUrl = d->openSearchStringForRequest(request);
566 }
567
568 // TODO request: check if entries is above pagesize*index, otherwise load next page.
569
570 QUrl url = d->currentUrl;
571 if (!url.isEmpty()) {
572 qCDebug(KNEWSTUFFCORE) << "requesting url" << url;
573 d->xmlLoader = new XmlLoader(this);
574 d->currentTime = QDateTime::currentDateTime();
575 d->loadingExtraDetails = false;
576 connect(sender: d->xmlLoader, signal: &XmlLoader::signalLoaded, context: this, slot: [this](const QDomDocument &doc) {
577 d->parseFeedData(doc);
578 });
579 connect(sender: d->xmlLoader, signal: &XmlLoader::signalFailed, context: this, slot: [this]() {
580 d->slotLoadingFailed();
581 });
582 d->xmlLoader->load(url);
583 } else {
584 Q_EMIT loadingFailed(request);
585 }
586 }
587}
588
589void OPDSProvider::loadEntryDetails(const Entry &entry)
590{
591 QUrl url;
592 QString entryMimeType = QStringList({OPDS_ATOM_MT, OPDS_TYPE_ENTRY, OPDS_PROFILE}).join(QStringLiteral(";"));
593 for (auto link : entry.downloadLinkInformationList()) {
594 if (link.tags.contains(t: KEY_MIME_TYPE + entryMimeType)) {
595 for (QString string : link.tags) {
596 if (string.startsWith(s: KEY_URL)) {
597 url = QUrl(string.split(QStringLiteral("=")).last());
598 }
599 }
600 }
601 }
602 if (!url.isEmpty()) {
603 d->xmlLoader = new XmlLoader(this);
604 d->currentTime = QDateTime::currentDateTime();
605 d->loadingExtraDetails = true;
606 connect(sender: d->xmlLoader, signal: &XmlLoader::signalLoaded, context: this, slot: [this](const QDomDocument &doc) {
607 d->parseFeedData(doc);
608 });
609 connect(sender: d->xmlLoader, signal: &XmlLoader::signalFailed, context: this, slot: [this]() {
610 d->slotLoadingFailed();
611 });
612 d->xmlLoader->load(url);
613 }
614}
615
616void OPDSProvider::loadPayloadLink(const KNSCore::Entry &entry, int linkNumber)
617{
618 KNSCore::Entry copy = entry;
619 for (auto downloadInfo : entry.downloadLinkInformationList()) {
620 if (downloadInfo.id == linkNumber) {
621 for (QString string : downloadInfo.tags) {
622 if (string.startsWith(s: KEY_URL)) {
623 copy.setPayload(string.split(QStringLiteral("=")).last());
624 }
625 }
626 }
627 }
628 Q_EMIT payloadLinkLoaded(copy);
629}
630
631bool OPDSProvider::setProviderXML(const QDomElement &xmldata)
632{
633 if (xmldata.tagName() != QLatin1String("provider")) {
634 return false;
635 }
636 d->providerId = xmldata.attribute(QStringLiteral("downloadurl"));
637
638 QUrl iconurl(xmldata.attribute(QStringLiteral("icon")));
639 if (!iconurl.isValid()) {
640 iconurl = QUrl::fromLocalFile(localfile: xmldata.attribute(QStringLiteral("icon")));
641 }
642 d->iconUrl = iconurl;
643
644 QDomNode n;
645 for (n = xmldata.firstChild(); !n.isNull(); n = n.nextSibling()) {
646 QDomElement e = n.toElement();
647 if (e.tagName() == QLatin1String("title")) {
648 d->providerName = e.text().trimmed();
649 }
650 }
651
652 d->currentUrl = QUrl(d->providerId);
653 QTimer::singleShot(interval: 0, receiver: this, slot: [this]() {
654 d->initialized = true;
655 Q_EMIT providerInitialized(this);
656 });
657 return true;
658}
659
660bool OPDSProvider::isInitialized() const
661{
662 return d->initialized;
663}
664
665void OPDSProvider::setCachedEntries(const KNSCore::Entry::List &cachedEntries)
666{
667 d->cachedEntries = cachedEntries;
668}
669}
670
671#include "moc_opdsprovider_p.cpp"
672

source code of knewstuff/src/opds/opdsprovider.cpp