6#include "opdsprovider_p.h"
9#include <KLocalizedString>
15#include <syndication/atom/atom.h>
16#include <syndication/documentsource.h>
18#include <knewstuffcore_debug.h>
20#include "tagsfilterchecker.h"
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"};
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"};
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"};
45static const QLatin1String OPDS_ATOM_MT{
"application/atom+xml"};
46static const QLatin1String OPDS_PROFILE{
"profile=opds-catalog"};
68static const QLatin1String OPENSEARCH_NS{
"http://a9.com/-/spec/opensearch/1.1/"};
69static const QLatin1String OPENSEARCH_MT{
"application/opensearchdescription+xml"};
74static const QLatin1String OPENSEARCH_ATTR_TEMPLATE{
"template"};
75static const QLatin1String OPENSEARCH_SEARCH_TERMS{
"searchTerms"};
77static const QLatin1String OPENSEARCH_START_INDEX{
"startIndex"};
86class OPDSProviderPrivate
89 OPDSProviderPrivate(OPDSProvider *qq)
92 , loadingExtraDetails(false)
110 bool loadingExtraDetails;
112 XmlLoader *xmlLoader;
114 Entry::List cachedEntries;
115 Provider::SearchRequest currentRequest;
117 QUrl openSearchDocumentURL;
124 QUrl searchUrl =
QUrl(openSearchTemplate);
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)) {
134 }
else if (key.second.contains(OPENSEARCH_START_PAGE)) {
136 }
else if (key.second.contains(OPENSEARCH_START_INDEX)) {
149 if (
query.isRelative()) {
150 if (selfUrl.
isEmpty() ||
QUrl(selfUrl).isRelative()) {
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) {
169void slotLoadingFailed()
171 qCWarning(KNEWSTUFFCORE) <<
"OPDS Loading failed for" << currentUrl;
172 Q_EMIT q->loadingFailed(currentRequest);
179 qCWarning(KNEWSTUFFCORE) <<
"Opensearch link does not point at document with opensearch namespace" << openSearchDocumentURL;
186 openSearchTemplate = el.
attribute(OPENSEARCH_ATTR_TEMPLATE);
208 QString fullEntryMimeType =
QStringList({OPDS_ATOM_MT, OPDS_TYPE_ENTRY, OPDS_PROFILE}).join(QStringLiteral(
";"));
210 if (!feedDoc->isValid()) {
211 qCWarning(KNEWSTUFFCORE) <<
"OPDS Feed at" << currentUrl <<
"not valid";
212 Q_EMIT q->loadingFailed(currentRequest);
215 if (!feedDoc->title().isEmpty()) {
216 providerName = feedDoc->title();
218 if (!feedDoc->icon().isEmpty()) {
219 iconUrl =
QUrl(fixRelativeUrl(feedDoc->icon()));
226 OPDSProvider::SearchPreset preset;
227 preset.providerId = providerId;
228 OPDSProvider::SearchRequest request;
229 request.searchTerm = providerId;
230 preset.request = request;
237 for (
auto link : feedDoc->links()) {
238 if (
link.rel().contains(REL_SELF)) {
239 selfUrl =
link.href();
243 for (
auto link : feedDoc->links()) {
245 if (
link.rel() == REL_SEARCH &&
link.type() == OPENSEARCH_MT) {
248 openSearchDocumentURL = fixRelativeUrl(theLink.href());
249 xmlLoader =
new XmlLoader(q);
252 q->d->parseOpenSearchDocument(doc);
255 qCWarning(KNEWSTUFFCORE) <<
"OpenSearch XML Document Loading failed" << openSearchDocumentURL;
259 &XmlLoader::signalHttpError,
264 static const QByteArray retryAfterKey{
"Retry-After"};
266 if (headerPair.first == retryAfterKey) {
279 osdUrlLoader(theLink);
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.",
294 xmlLoader->load(openSearchDocumentURL);
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) {
322 preset.type = Provider::SearchPresetTypes::NoPresetType;
323 if (preset.displayName.isEmpty()) {
324 preset.displayName =
link.rel();
330 TagsFilterChecker downloadTagChecker(q->downloadTagFilter());
331 TagsFilterChecker entryTagChecker(q->tagFilter());
333 for (
int i = 0; i < feedDoc->entries().size(); i++) {
337 entry.setName(feedEntry.
title());
338 entry.setProviderId(providerId);
339 entry.setUniqueId(feedEntry.
id());
341 entry.setStatus(KNSCore::Entry::Invalid);
342 for (
const Entry &cachedEntry : std::as_const(cachedEntries)) {
343 if (entry.uniqueId() == cachedEntry.uniqueId()) {
352 for (
int j = 0; j < feedEntry.
categories().size(); j++) {
359 if (entryTagChecker.filterAccepts(entryTags)) {
360 entry.setTags(entryTags);
365 for (
int j = 0; j < feedEntry.
authors().size(); 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);
374 entry.setLicense(feedEntry.
rights());
380 entry.setShortSummary(feedEntry.
summary());
382 int counterThumbnails = 0;
383 int counterImages = 0;
385 for (
int j = 0; j < feedEntry.
links().size(); j++) {
388 KNSCore::Entry::DownloadLinkInformation download;
389 download.id = entry.downloadLinkCount() + 1;
395 if (!
link.hrefLanguage().isEmpty()) {
396 tags.
append(KEY_LANGUAGE +
link.hrefLanguage());
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;
405 if (
link.rel().startsWith(OPDS_REL_ACQUISITION)) {
406 if (
link.title().isEmpty()) {
409 l.
append(QStringLiteral(
"(") +
link.rel().split(QStringLiteral(
"/")).last() + QStringLiteral(
")"));
410 download.name = l.
join(QStringLiteral(
" "));
413 if (!downloadTagChecker.filterAccepts(download.tags)) {
417 if (linkRelation.
contains(OPDS_REL_AC_BORROW) || linkRelation.
contains(OPDS_REL_AC_SUBSCRIBE) || linkRelation.
contains(OPDS_REL_AC_BUY)) {
421 }
else if (linkRelation.
contains(OPDS_REL_ACQUISITION) || linkRelation.
contains(OPDS_REL_AC_OPEN_ACCESS)) {
422 download.isDownloadtypeLink =
true;
424 if (entry.status() != KNSCore::Entry::Installed && entry.status() != KNSCore::Entry::Updateable) {
425 entry.setStatus(KNSCore::Entry::Downloadable);
432 for (
QDomElement el : feedEntry.elementsByTagName(OPDS_EL_PRICE)) {
437 entry.appendDownloadLinkInformation(download);
439 }
else if (
link.rel().startsWith(OPDS_REL_IMAGE)) {
440 if (
link.rel() == OPDS_REL_THUMBNAIL) {
441 entry.setPreviewUrl(linkUrl, KNSCore::Entry::PreviewType(counterThumbnails));
442 counterThumbnails += 1;
444 entry.setPreviewUrl(linkUrl, KNSCore::Entry::PreviewType(counterImages + 3));
452 if (
link.type().startsWith(OPDS_ATOM_MT)) {
453 if (
link.type() == fullEntryMimeType) {
454 entry.appendDownloadLinkInformation(download);
456 groupEntryUrl = linkUrl;
459 }
else if (
link.type() == HTML_MT && linkRelation.
contains(REL_ALTERNATE)) {
460 entry.setHomepage(
QUrl(linkUrl));
462 }
else if (downloadTagChecker.filterAccepts(download.tags)) {
463 entry.appendDownloadLinkInformation(download);
476 if (entry.releaseDate().isNull()) {
477 entry.setReleaseDate(date.
date());
480 if (entry.status() != KNSCore::Entry::Invalid) {
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);
494 if (counterThumbnails == 0) {
496 if (!feedDoc->icon().isEmpty()) {
497 entry.setPreviewUrl(fixRelativeUrl(feedDoc->icon()).
toString());
501 if (entry.downloadLinkCount() == 0) {
506 entry.setPayload(groupEntryUrl);
510 entries.append(entry);
513 if (loadingExtraDetails) {
514 Q_EMIT q->entryDetailsLoaded(entries.first());
515 loadingExtraDetails =
false;
517 Q_EMIT q->loadingFinished(currentRequest, entries);
519 Q_EMIT q->searchPresetsLoaded(presets);
524OPDSProvider::OPDSProvider()
525 : d(new OPDSProviderPrivate(this))
529OPDSProvider::~OPDSProvider() =
default;
531QString OPDSProvider::id()
const
533 return d->providerId;
536QString OPDSProvider::name()
const
538 return d->providerName;
541QUrl OPDSProvider::icon()
const
548 d->currentRequest = request;
550 if (request.filter == Installed) {
551 Q_EMIT loadingFinished(request, d->installedEntries());
553 }
else if (request.filter == Provider::ExactEntryId) {
554 for (Entry entry : d->cachedEntries) {
555 if (entry.uniqueId() == request.searchTerm) {
556 loadEntryDetails(entry);
561 d->currentUrl =
QUrl(request.searchTerm);
562 }
else if (!d->openSearchTemplate.isEmpty() && !request.searchTerm.
isEmpty()) {
565 d->currentUrl = d->openSearchStringForRequest(request);
570 QUrl url = d->currentUrl;
572 qCDebug(KNEWSTUFFCORE) <<
"requesting url" << url;
573 d->xmlLoader =
new XmlLoader(
this);
575 d->loadingExtraDetails =
false;
577 d->parseFeedData(doc);
579 connect(d->xmlLoader, &XmlLoader::signalFailed,
this, [
this]() {
580 d->slotLoadingFailed();
582 d->xmlLoader->load(url);
584 Q_EMIT loadingFailed(request);
589void OPDSProvider::loadEntryDetails(
const Entry &entry)
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)) {
596 if (
string.startsWith(KEY_URL)) {
597 url =
QUrl(
string.split(QStringLiteral(
"=")).last());
603 d->xmlLoader =
new XmlLoader(
this);
605 d->loadingExtraDetails =
true;
607 d->parseFeedData(doc);
609 connect(d->xmlLoader, &XmlLoader::signalFailed,
this, [
this]() {
610 d->slotLoadingFailed();
612 d->xmlLoader->load(url);
616void OPDSProvider::loadPayloadLink(
const KNSCore::Entry &entry,
int linkNumber)
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());
628 Q_EMIT payloadLinkLoaded(copy);
631bool OPDSProvider::setProviderXML(
const QDomElement &xmldata)
636 d->providerId = xmldata.
attribute(QStringLiteral(
"downloadurl"));
639 if (!iconurl.isValid()) {
642 d->iconUrl = iconurl;
652 d->currentUrl =
QUrl(d->providerId);
654 d->initialized =
true;
655 Q_EMIT providerInitialized(
this);
660bool OPDSProvider::isInitialized()
const
662 return d->initialized;
667 d->cachedEntries = cachedEntries;
671#include "moc_opdsprovider_p.cpp"
KNewStuff data entry container.
@ GroupEntry
these are entries whose payload is another feed. Currently only used by the OPDS provider.
@ CatalogEntry
These are the main entries that KNewStuff can get the details about and download links for.
@ Root
preset indicating a root directory.
@ Start
preset indicating the first entry.
@ New
preset indicating new items.
@ Subscription
preset indicating items that the user is subscribed to.
@ Shelf
preset indicating previously acquired items.
@ FolderUp
preset indicating going up in the search result hierarchy.
@ AllEntries
preset indicating all possible entries, such as a crawlable list. Might be intense to load.
@ Popular
preset indicating popular items.
@ Recommended
preset for recommended. This may be customized by the server per user.
@ Featured
preset for featured items.
bool isEscapedHTML() const
QList< Person > authors() const
QList< Category > categories() const
QList< Link > links() const
Syndication::SpecificDocumentPtr parse(const Syndication::DocumentSource &source) const override
QString childNodesAsXML() const
Q_SCRIPTABLE CaptureState status()
QString i18n(const char *text, const TYPE &arg...)
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
KIOCORE_EXPORT CopyJob * copy(const QList< QUrl > &src, const QUrl &dest, JobFlags flags=DefaultFlags)
KIOCORE_EXPORT CopyJob * link(const QList< QUrl > &src, const QUrl &destDir, JobFlags flags=DefaultFlags)
QDateTime currentDateTime()
qint64 currentMSecsSinceEpoch()
qint64 currentSecsSinceEpoch()
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
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
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
used to keep track of a search