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

KDE's Doxygen guidelines are available online.