KNewStuff

opdsprovider.cpp
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
128
129 for (QPair<QString, QString> key : templateQuery.queryItems()) {
130 if (key.second.contains(OPENSEARCH_SEARCH_TERMS)) {
131 query.addQueryItem(key.first, request.searchTerm);
132 } else if (key.second.contains(OPENSEARCH_COUNT)) {
133 query.addQueryItem(key.first, QString::number(request.pageSize));
134 } else if (key.second.contains(OPENSEARCH_START_PAGE)) {
135 query.addQueryItem(key.first, QString::number(request.page));
136 } else if (key.second.contains(OPENSEARCH_START_INDEX)) {
137 query.addQueryItem(key.first, 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 {
149 if (query.isRelative()) {
150 if (selfUrl.isEmpty() || QUrl(selfUrl).isRelative()) {
151 return currentUrl.resolved(query);
152 } else {
153 return QUrl(selfUrl).resolved(query);
154 }
155 }
156 return query;
157 };
158
159 Entry::List installedEntries() const {{Entry::List entries;
160 for (const Entry &entry : std::as_const(cachedEntries)) {
161 if (entry.status() == KNSCore::Entry::Installed || entry.status() == KNSCore::Entry::Updateable) {
162 entries.append(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(OPENSEARCH_EL_URL);
183while (!el.isNull()) {
184 if (el.attribute(OPENSEARCH_ATTR_TYPE).contains(OPDS_ATOM_MT)) {
185 if (openSearchTemplate.isEmpty() || el.attribute(OPENSEARCH_ATTR_TYPE).contains(OPDS_PROFILE)) {
186 openSearchTemplate = el.attribute(OPENSEARCH_ATTR_TEMPLATE);
187 }
188 }
189
190 el = el.nextSiblingElement(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(feedDoc->icon()));
220 }
221
222 Entry::List entries;
224
225 {
226 OPDSProvider::SearchPreset preset;
227 preset.providerId = providerId;
228 OPDSProvider::SearchRequest request;
229 request.searchTerm = providerId;
230 preset.request = request;
232 presets.append(preset);
233 }
234
235 // find the self link first!
236 selfUrl.clear();
237 for (auto link : feedDoc->links()) {
238 if (link.rel().contains(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) {
248 openSearchDocumentURL = fixRelativeUrl(theLink.href());
249 xmlLoader = new XmlLoader(q);
250
251 QObject::connect(xmlLoader, &XmlLoader::signalLoaded, q, [this](const QDomDocument &doc) {
252 q->d->parseOpenSearchDocument(doc);
253 });
254 QObject::connect(xmlLoader, &XmlLoader::signalFailed, q, [this]() {
255 qCWarning(KNEWSTUFFCORE) << "OpenSearch XML Document Loading failed" << openSearchDocumentURL;
256 });
258 xmlLoader,
260 q,
261 [this, &osdUrlLoader, theLink](int status, QList<QNetworkReply::RawHeaderPair> rawHeaders) { // clazy:exclude=lambda-in-connect
262 if (status == 503) { // Temporarily Unavailable
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...)
272 dummyRequest.setRawHeader(QByteArray{"Last-Modified"}, headerPair.second);
274 break;
275 }
276 }
277 // clazy:exclude=lambda-in-connect
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 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 {retryAfter});
290 }
291 }
292 });
293
294 xmlLoader->load(openSearchDocumentURL);
295 };
296 } else if (link.type().contains(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(link.href()).toString();
302 preset.request = request;
303 if (link.rel() == REL_START) {
305 } else if (link.rel() == OPDS_REL_FEATURED) {
307 } else if (link.rel() == OPDS_REL_SHELF) {
309 } else if (link.rel() == OPDS_REL_SORT_NEW) {
311 } else if (link.rel() == OPDS_REL_SORT_POPULAR) {
313 } else if (link.rel() == REL_UP) {
315 } else if (link.rel() == OPDS_REL_CRAWL) {
317 } else if (link.rel() == OPDS_REL_RECOMMENDED) {
319 } else if (link.rel() == OPDS_REL_SUBSCRIPTIONS) {
321 } else {
322 preset.type = Provider::SearchPresetTypes::NoPresetType;
323 if (preset.displayName.isEmpty()) {
324 preset.displayName = link.rel();
325 }
326 }
327 presets.append(preset);
328 }
329 }
330 TagsFilterChecker downloadTagChecker(q->downloadTagFilter());
331 TagsFilterChecker entryTagChecker(q->tagFilter());
332
333 for (int i = 0; i < feedDoc->entries().size(); 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(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...
352 for (int j = 0; j < feedEntry.categories().size(); j++) {
353 QString tag = feedEntry.categories().at(j).label();
354 if (tag.isEmpty()) {
355 tag = feedEntry.categories().at(j).term();
356 }
357 entryTags.append(tag);
358 }
359 if (entryTagChecker.filterAccepts(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(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;
385 for (int j = 0; j < feedEntry.links().size(); 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(KEY_MIME_TYPE + link.type());
395 if (!link.hrefLanguage().isEmpty()) {
396 tags.append(KEY_LANGUAGE + link.hrefLanguage());
397 }
398 QString linkUrl = fixRelativeUrl(link.href()).toString();
399 tags.append(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(OPDS_REL_ACQUISITION)) {
406 if (link.title().isEmpty()) {
407 QStringList l;
408 l.append(link.type());
409 l.append(QStringLiteral("(") + link.rel().split(QStringLiteral("/")).last() + QStringLiteral(")"));
410 download.name = l.join(QStringLiteral(" "));
411 }
412
413 if (!downloadTagChecker.filterAccepts(download.tags)) {
414 continue;
415 }
416
417 if (linkRelation.contains(OPDS_REL_AC_BORROW) || linkRelation.contains(OPDS_REL_AC_SUBSCRIBE) || linkRelation.contains(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(OPDS_REL_ACQUISITION) || linkRelation.contains(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(OPDS_EL_PRICE)) {
433 QLocale locale;
434 download.priceAmount = locale.toCurrencyString(el.text().toFloat(), el.attribute(ATTR_CURRENCY_CODE));
435 }
436 // There's an 'opds:indirectaquistition' element that gives extra metadata about bundles.
437 entry.appendDownloadLinkInformation(download);
438
439 } else if (link.rel().startsWith(OPDS_REL_IMAGE)) {
440 if (link.rel() == OPDS_REL_THUMBNAIL) {
441 entry.setPreviewUrl(linkUrl, KNSCore::Entry::PreviewType(counterThumbnails));
443 } else {
444 entry.setPreviewUrl(linkUrl, 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(OPDS_ATOM_MT)) {
453 if (link.type() == fullEntryMimeType) {
454 entry.appendDownloadLinkInformation(download);
455 } else {
457 }
458
459 } else if (link.type() == HTML_MT && linkRelation.contains(REL_ALTERNATE)) {
460 entry.setHomepage(QUrl(linkUrl));
461
462 } else if (downloadTagChecker.filterAccepts(download.tags)) {
463 entry.appendDownloadLinkInformation(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
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(fixRelativeUrl(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(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
525 : d(new OPDSProviderPrivate(this))
526{
527}
528
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(d->xmlLoader, &XmlLoader::signalLoaded, this, [this](const QDomDocument &doc) {
577 d->parseFeedData(doc);
578 });
579 connect(d->xmlLoader, &XmlLoader::signalFailed, this, [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(KEY_MIME_TYPE + entryMimeType)) {
595 for (QString string : link.tags) {
596 if (string.startsWith(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(d->xmlLoader, &XmlLoader::signalLoaded, this, [this](const QDomDocument &doc) {
607 d->parseFeedData(doc);
608 });
609 connect(d->xmlLoader, &XmlLoader::signalFailed, this, [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(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(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(0, this, [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"
KNewStuff data entry container.
Definition entry.h:48
@ GroupEntry
these are entries whose payload is another feed. Currently only used by the OPDS provider.
Definition entry.h:135
@ CatalogEntry
These are the main entries that KNewStuff can get the details about and download links for.
Definition entry.h:134
@ Root
preset indicating a root directory.
Definition provider.h:119
@ Start
preset indicating the first entry.
Definition provider.h:120
@ New
preset indicating new items.
Definition provider.h:126
@ Subscription
preset indicating items that the user is subscribed to.
Definition provider.h:125
@ Shelf
preset indicating previously acquired items.
Definition provider.h:124
@ FolderUp
preset indicating going up in the search result hierarchy.
Definition provider.h:127
@ AllEntries
preset indicating all possible entries, such as a crawlable list. Might be intense to load.
Definition provider.h:128
@ Popular
preset indicating popular items.
Definition provider.h:121
@ Recommended
preset for recommended. This may be customized by the server per user.
Definition provider.h:123
@ Featured
preset for featured items.
Definition provider.h:122
Q_SCRIPTABLE CaptureState status()
QString i18n(const char *text, const TYPE &arg...)
std::optional< QSqlQuery > query(const QString &queryStatement)
KIOCORE_EXPORT CopyJob * link(const QList< QUrl > &src, const QUrl &destDir, JobFlags flags=DefaultFlags)
QAction * copy(const QObject *recvr, const char *slot, QObject *parent)
QDateTime currentDateTime()
qint64 currentMSecsSinceEpoch()
qint64 currentSecsSinceEpoch()
QDate date() const const
QDateTime fromSecsSinceEpoch(qint64 secs)
qint64 secsTo(const QDateTime &other) const const
QDomElement documentElement() const const
QByteArray toByteArray(int indent) const const
QString attribute(const QString &name, const QString &defValue) const const
QString tagName() const const
QString text() const const
QDomElement firstChildElement(const QString &tagName, const QString &namespaceURI) const const
bool isNull() const const
QDomNode nextSibling() const const
QDomElement toElement() const const
void append(QList< T > &&value)
QString toCurrencyString(double value, const QString &symbol, int precision) const const
typedef RawHeaderPair
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool isNull() const const
QSharedPointer< X > staticCast() const const
void clear()
bool isEmpty() const const
QString number(double n, char format, int precision)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QString trimmed() const const
QString join(QChar separator) const const
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QUrl fromLocalFile(const QString &localFile)
bool isEmpty() const const
QUrl resolved(const QUrl &relative) const const
QString scheme() const const
QString toString(FormattingOptions options) const const
used to keep track of a search
Definition provider.h:70
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 24 2024 11:54:38 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.