File indexing completed on 2024-04-28 04:49:04

0001 /*
0002    SPDX-FileCopyrightText: 2015 (c) Matthieu Gallien <matthieu_gallien@yahoo.fr>
0003 
0004    SPDX-License-Identifier: LGPL-3.0-or-later
0005  */
0006 
0007 #include "didlparser.h"
0008 
0009 #include "upnpcontrolcontentdirectory.h"
0010 #include "upnpcontrolabstractservicereply.h"
0011 #include "upnpservicedescription.h"
0012 #include "upnpdevicedescription.h"
0013 #include "elisautils.h"
0014 
0015 #include "upnpLogging.h"
0016 
0017 #include <QVector>
0018 #include <QString>
0019 
0020 #include <QDomDocument>
0021 #include <QDomNode>
0022 
0023 class DidlParserPrivate
0024 {
0025 public:
0026 
0027     QString mBrowseFlag = QStringLiteral("*");
0028 
0029     QString mFilter = QStringLiteral("*");
0030 
0031     QString mSortCriteria;
0032 
0033     QString mSearchCriteria;
0034 
0035     QString mParentId;
0036 
0037     QString mDeviceUUID;
0038 
0039     UpnpControlContentDirectory *mContentDirectory = nullptr;
0040 
0041     QVector<QString> mNewMusicTrackIds;
0042 
0043     QHash<QString, DataTypes::UpnpTrackDataType> mNewMusicTracks;
0044 
0045     QHash<QString, QVector<DataTypes::UpnpTrackDataType>> mNewTracksByAlbums;
0046 
0047     QHash<QString, QUrl> mCovers;
0048 
0049     bool mIsDataValid = false;
0050 
0051 };
0052 
0053 DidlParser::DidlParser(QObject *parent) : QObject(parent), d(new DidlParserPrivate)
0054 {
0055 }
0056 
0057 DidlParser::~DidlParser()
0058 = default;
0059 
0060 const QString &DidlParser::browseFlag() const
0061 {
0062     return d->mBrowseFlag;
0063 }
0064 
0065 const QString &DidlParser::filter() const
0066 {
0067     return d->mFilter;
0068 }
0069 
0070 const QString &DidlParser::sortCriteria() const
0071 {
0072     return d->mSortCriteria;
0073 }
0074 
0075 const QString &DidlParser::searchCriteria() const
0076 {
0077     return d->mSearchCriteria;
0078 }
0079 
0080 UpnpControlContentDirectory *DidlParser::contentDirectory() const
0081 {
0082     return d->mContentDirectory;
0083 }
0084 
0085 bool DidlParser::isDataValid() const
0086 {
0087     return d->mIsDataValid;
0088 }
0089 
0090 void DidlParser::setBrowseFlag(QString flag)
0091 {
0092     if (d->mBrowseFlag == flag) {
0093         return;
0094     }
0095 
0096     d->mBrowseFlag = std::move(flag);
0097     Q_EMIT browseFlagChanged();
0098 }
0099 
0100 void DidlParser::setFilter(QString flag)
0101 {
0102     if (d->mFilter == flag) {
0103         return;
0104     }
0105 
0106     d->mFilter = std::move(flag);
0107     Q_EMIT filterChanged();
0108 }
0109 
0110 void DidlParser::setSortCriteria(QString criteria)
0111 {
0112     if (d->mSortCriteria == criteria) {
0113         return;
0114     }
0115 
0116     d->mSortCriteria = std::move(criteria);
0117     Q_EMIT sortCriteriaChanged();
0118 }
0119 
0120 void DidlParser::setSearchCriteria(QString criteria)
0121 {
0122     if (d->mSearchCriteria == criteria) {
0123         return;
0124     }
0125 
0126     d->mSearchCriteria = std::move(criteria);
0127     Q_EMIT searchCriteriaChanged();
0128 }
0129 
0130 void DidlParser::setContentDirectory(UpnpControlContentDirectory *directory)
0131 {
0132     d->mContentDirectory = directory;
0133 
0134     if (!d->mContentDirectory) {
0135         Q_EMIT contentDirectoryChanged();
0136         return;
0137     }
0138 
0139     Q_EMIT contentDirectoryChanged();
0140 }
0141 
0142 void DidlParser::setParentId(QString parentId)
0143 {
0144     if (d->mParentId == parentId) {
0145         return;
0146     }
0147 
0148     d->mParentId = std::move(parentId);
0149     Q_EMIT parentIdChanged();
0150 }
0151 
0152 void DidlParser::setDeviceUUID(QString deviceUUID)
0153 {
0154     if (d->mDeviceUUID == deviceUUID) {
0155         return;
0156     }
0157 
0158     d->mDeviceUUID = std::move(deviceUUID);
0159     Q_EMIT deviceUUIDChanged();
0160 }
0161 
0162 void DidlParser::systemUpdateIDChanged()
0163 {
0164     search();
0165 }
0166 
0167 void DidlParser::browse(int startIndex, int maximumNmberOfResults)
0168 {
0169     qCDebug(orgKdeElisaUpnp()) << "DidlParser::browse" << d->mParentId << d->mBrowseFlag << d->mFilter << startIndex << maximumNmberOfResults << d->mSortCriteria;
0170 
0171     auto upnpAnswer = d->mContentDirectory->browse(d->mParentId, d->mBrowseFlag, d->mFilter, startIndex, maximumNmberOfResults, d->mSortCriteria);
0172 
0173     if (startIndex == 0) {
0174         d->mNewMusicTracks.clear();
0175         d->mNewMusicTrackIds.clear();
0176         d->mCovers.clear();
0177     }
0178 
0179     connect(upnpAnswer, &UpnpControlAbstractServiceReply::finished, this, &DidlParser::browseFinished);
0180 }
0181 
0182 void DidlParser::search(int startIndex, int maximumNumberOfResults)
0183 {
0184     if (!d->mContentDirectory) {
0185         return;
0186     }
0187 
0188     if (startIndex == 0) {
0189         d->mNewMusicTracks.clear();
0190         d->mNewMusicTrackIds.clear();
0191         d->mCovers.clear();
0192     }
0193 
0194     auto upnpAnswer = d->mContentDirectory->search(d->mParentId, d->mSearchCriteria, d->mFilter, startIndex, maximumNumberOfResults, d->mSortCriteria);
0195 
0196     connect(upnpAnswer, &UpnpControlAbstractServiceReply::finished, this, &DidlParser::searchFinished);
0197 }
0198 
0199 QString DidlParser::parentId() const
0200 {
0201     return d->mParentId;
0202 }
0203 
0204 const QString &DidlParser::deviceUUID() const
0205 {
0206     return d->mDeviceUUID;
0207 }
0208 
0209 const QVector<QString> &DidlParser::newMusicTrackIds() const
0210 {
0211     return d->mNewMusicTrackIds;
0212 }
0213 
0214 const QHash<QString, DataTypes::UpnpTrackDataType> &DidlParser::newMusicTracks() const
0215 {
0216     return d->mNewMusicTracks;
0217 }
0218 
0219 const QHash<QString, QUrl> &DidlParser::covers() const
0220 {
0221     return d->mCovers;
0222 }
0223 
0224 void DidlParser::browseFinished(UpnpControlAbstractServiceReply *self)
0225 {
0226     qCDebug(orgKdeElisaUpnp()) << "DidlParser::browseFinished";
0227 
0228     const auto &resultData = self->result();
0229 
0230     bool success = self->success();
0231 
0232     if (!success) {
0233         qCDebug(orgKdeElisaUpnp()) << "DidlParser::browseFinished" << "error" << self->error();
0234 
0235         d->mIsDataValid = false;
0236         Q_EMIT isDataValidChanged(d->mParentId);
0237 
0238         return;
0239     }
0240 
0241     QString result = resultData[QStringLiteral("Result")].toString();
0242 
0243     bool intConvert;
0244     auto numberReturned = resultData[QStringLiteral("NumberReturned")].toInt(&intConvert);
0245 
0246     if (!intConvert) {
0247         d->mIsDataValid = false;
0248         Q_EMIT isDataValidChanged(d->mParentId);
0249 
0250         return;
0251     }
0252 
0253     auto totalMatches = resultData[QStringLiteral("TotalMatches")].toInt(&intConvert);
0254 
0255     if (!intConvert) {
0256         d->mIsDataValid = false;
0257         Q_EMIT isDataValidChanged(d->mParentId);
0258 
0259         return;
0260     }
0261 
0262     if (totalMatches > numberReturned) {
0263         browse(d->mNewMusicTracks.size() + numberReturned);
0264     }
0265 
0266     QDomDocument browseDescription;
0267     browseDescription.setContent(result);
0268 
0269     browseDescription.documentElement();
0270 
0271     auto containerList = browseDescription.elementsByTagName(QStringLiteral("container"));
0272     for (int containerIndex = 0; containerIndex < containerList.length(); ++containerIndex) {
0273         const QDomNode &containerNode(containerList.at(containerIndex));
0274         if (!containerNode.isNull()) {
0275             decodeContainerNode(containerNode, d->mNewMusicTracks, d->mNewMusicTrackIds);
0276         }
0277     }
0278 
0279     auto itemList = browseDescription.elementsByTagName(QStringLiteral("item"));
0280     for (int itemIndex = 0; itemIndex < itemList.length(); ++itemIndex) {
0281         const QDomNode &itemNode(itemList.at(itemIndex));
0282         if (!itemNode.isNull()) {
0283             qCInfo(orgKdeElisaUpnp()) << "DidlParser::browseFinished" << "new track node" << itemNode.toDocument().toString();
0284 
0285             decodeAudioTrackNode(itemNode, d->mNewMusicTracks, d->mNewMusicTrackIds);
0286         }
0287     }
0288 
0289     groupNewTracksByAlbums();
0290     d->mIsDataValid = true;
0291     Q_EMIT isDataValidChanged(d->mParentId);
0292 }
0293 
0294 void DidlParser::groupNewTracksByAlbums()
0295 {
0296     d->mNewTracksByAlbums.clear();
0297     for(const auto &newTrack : std::as_const(d->mNewMusicTracks)) {
0298         d->mNewTracksByAlbums[newTrack.album()].push_back(newTrack);
0299     }
0300 }
0301 
0302 void DidlParser::searchFinished(UpnpControlAbstractServiceReply *self)
0303 {
0304     qCDebug(orgKdeElisaUpnp()) << "DidlParser::searchFinished";
0305 
0306     const auto &resultData = self->result();
0307 
0308     bool success = self->success();
0309 
0310     if (!success) {
0311         qCDebug(orgKdeElisaUpnp()) << "DidlParser::searchFinished" << "error" << self->error();
0312 
0313         d->mIsDataValid = false;
0314         Q_EMIT isDataValidChanged(d->mParentId);
0315 
0316         return;
0317     }
0318 
0319     QString result = resultData[QStringLiteral("Result")].toString();
0320 
0321     qCDebug(orgKdeElisaUpnp()) << "DidlParser::searchFinished" << "result" << result;
0322 
0323     bool intConvert;
0324     auto numberReturned = resultData[QStringLiteral("NumberReturned")].toInt(&intConvert);
0325 
0326     qCDebug(orgKdeElisaUpnp()) << "DidlParser::searchFinished" << "NumberReturned" << numberReturned;
0327 
0328     if (!intConvert) {
0329         d->mIsDataValid = false;
0330         Q_EMIT isDataValidChanged(d->mParentId);
0331 
0332         return;
0333     }
0334 
0335     auto totalMatches = resultData[QStringLiteral("TotalMatches")].toInt(&intConvert);
0336 
0337     qCDebug(orgKdeElisaUpnp()) << "DidlParser::searchFinished" << "TotalMatches" << totalMatches;
0338 
0339     if (!intConvert) {
0340         d->mIsDataValid = false;
0341         Q_EMIT isDataValidChanged(d->mParentId);
0342 
0343         return;
0344     }
0345 
0346     if (totalMatches > numberReturned) {
0347         search(d->mNewMusicTracks.size() + numberReturned, numberReturned);
0348     }
0349 
0350     QDomDocument browseDescription;
0351     browseDescription.setContent(result);
0352 
0353     browseDescription.documentElement();
0354 
0355     auto containerList = browseDescription.elementsByTagName(QStringLiteral("container"));
0356     for (int containerIndex = 0; containerIndex < containerList.length(); ++containerIndex) {
0357         const QDomNode &containerNode(containerList.at(containerIndex));
0358         if (!containerNode.isNull()) {
0359             decodeContainerNode(containerNode, d->mNewMusicTracks, d->mNewMusicTrackIds);
0360         }
0361     }
0362 
0363     auto itemList = browseDescription.elementsByTagName(QStringLiteral("item"));
0364     for (int itemIndex = 0; itemIndex < itemList.length(); ++itemIndex) {
0365         const QDomNode &itemNode(itemList.at(itemIndex));
0366         if (!itemNode.isNull()) {
0367             decodeAudioTrackNode(itemNode, d->mNewMusicTracks, d->mNewMusicTrackIds);
0368         }
0369     }
0370 
0371     groupNewTracksByAlbums();
0372     d->mIsDataValid = true;
0373     Q_EMIT isDataValidChanged(d->mParentId);
0374 }
0375 
0376 void DidlParser::decodeContainerNode(const QDomNode &containerNode, QHash<QString, DataTypes::UpnpTrackDataType> &newData,
0377                                      QVector<QString> &newDataIds)
0378 {
0379     qCDebug(orgKdeElisaUpnp()) << "DidlParser::decodeContainerNode";
0380 
0381     auto allChilds = containerNode.childNodes();
0382 
0383     for(int i = 0; i < allChilds.count(); ++i) {
0384         const auto &oneChild = allChilds.at(i);
0385         qCDebug(orgKdeElisaUpnp()) << "DidlParser::decodeContainerNode" << oneChild.nodeName();
0386     }
0387 
0388     auto parentID = containerNode.toElement().attribute(QStringLiteral("parentID"));
0389     const auto &id = containerNode.toElement().attribute(QStringLiteral("id"));
0390 
0391     newDataIds.push_back(id);
0392     auto &childData = newData[id];
0393 
0394     childData[DataTypes::ColumnsRoles::ParentIdRole] = parentID;
0395     childData[DataTypes::ColumnsRoles::IdRole] = id;
0396 
0397     const QString &childCount = containerNode.toElement().attribute(QStringLiteral("childCount"));
0398     childData[DataTypes::ColumnsRoles::ChildCountRole] = childCount.toInt();
0399 
0400     const QDomNode &titleNode = containerNode.firstChildElement(QStringLiteral("dc:title"));
0401     if (!titleNode.isNull()) {
0402         childData[DataTypes::ColumnsRoles::TitleRole] = titleNode.toElement().text();
0403     }
0404 
0405     const QDomNode &authorNode = containerNode.firstChildElement(QStringLiteral("upnp:artist"));
0406     if (!authorNode.isNull()) {
0407         childData[DataTypes::ColumnsRoles::ArtistRole] = authorNode.toElement().text();
0408     }
0409 
0410     const QDomNode &resourceNode = containerNode.firstChildElement(QStringLiteral("res"));
0411     if (!resourceNode.isNull()) {
0412         childData[DataTypes::ColumnsRoles::ResourceRole] = QUrl::fromUserInput(resourceNode.toElement().text());
0413     }
0414 
0415     const QDomNode &classNode = containerNode.firstChildElement(QStringLiteral("upnp:class"));
0416     qCDebug(orgKdeElisaUpnp()) << "DidlParser::decodeContainerNode" << "upnp:class" << classNode.toElement().text();
0417 //    if (classNode.toElement().text().startsWith(QLatin1String("object.container.album.musicAlbum"))) {
0418 //        childData[DataTypes::ElementTypeRole] = QVariant::fromValue(ElisaUtils::Album);
0419 //    } else if (classNode.toElement().text().startsWith(QLatin1String("object.container.person.musicArtist"))) {
0420 //        childData[DataTypes::ElementTypeRole] = QVariant::fromValue(ElisaUtils::Artist);
0421 //    } else if (classNode.toElement().text().startsWith(QLatin1String("object.container"))) {
0422         childData[DataTypes::ElementTypeRole] = QVariant::fromValue(ElisaUtils::UpnpMediaServer);
0423         childData[DataTypes::UUIDRole] = d->mDeviceUUID;
0424 //    }
0425 
0426     const QDomNode &albumArtNode = containerNode.firstChildElement(QStringLiteral("upnp:albumArtURI"));
0427     if (!albumArtNode.isNull()) {
0428         childData[DataTypes::ColumnsRoles::ImageUrlRole] = QUrl::fromUserInput(albumArtNode.toElement().text());
0429     }
0430 
0431     qCDebug(orgKdeElisaUpnp()) << "DidlParser::decodeContainerNode" << childData;
0432 }
0433 
0434 void DidlParser::decodeAudioTrackNode(const QDomNode &itemNode, QHash<QString, DataTypes::UpnpTrackDataType> &newData,
0435                                       QVector<QString> &newDataIds)
0436 {
0437     qCDebug(orgKdeElisaUpnp()) << "DidlParser::decodeAudioTrackNode";
0438 
0439     auto allChilds = itemNode.childNodes();
0440 
0441     for(int i = 0; i < allChilds.count(); ++i) {
0442         const auto &oneChild = allChilds.at(i);
0443         qCDebug(orgKdeElisaUpnp()) << "DidlParser::decodeAudioTrackNode" << oneChild.nodeName();
0444     }
0445 
0446     const QString &parentID = itemNode.toElement().attribute(QStringLiteral("parentID"));
0447     const QString &id = itemNode.toElement().attribute(QStringLiteral("id"));
0448 
0449     newDataIds.push_back(id);
0450     auto &childData = newData[id];
0451 
0452     childData[DataTypes::ElementTypeRole] = QVariant::fromValue(ElisaUtils::Track);
0453     childData[DataTypes::ColumnsRoles::ParentIdRole] = parentID;
0454     childData[DataTypes::ColumnsRoles::IdRole] = id;
0455 
0456     const QDomNode &titleNode = itemNode.firstChildElement(QStringLiteral("dc:title"));
0457     if (!titleNode.isNull()) {
0458         childData[DataTypes::ColumnsRoles::TitleRole] = titleNode.toElement().text();
0459     }
0460 
0461     const QDomNode &authorNode = itemNode.firstChildElement(QStringLiteral("dc:creator"));
0462     if (!authorNode.isNull()) {
0463         childData[DataTypes::ColumnsRoles::ArtistRole] = authorNode.toElement().text();
0464     }
0465 
0466     const QDomNode &albumAuthorNode = itemNode.firstChildElement(QStringLiteral("upnp:artist"));
0467     if (!albumAuthorNode.isNull()) {
0468         childData[DataTypes::ColumnsRoles::AlbumArtistRole] = albumAuthorNode.toElement().text();
0469     }
0470 
0471     if (childData.albumArtist().isEmpty()) {
0472         childData[DataTypes::ColumnsRoles::AlbumArtistRole] = childData.artist();
0473     }
0474 
0475     if (childData.artist().isEmpty()) {
0476         childData[DataTypes::ColumnsRoles::ArtistRole] = childData.albumArtist();
0477     }
0478 
0479     const QDomNode &albumNode = itemNode.firstChildElement(QStringLiteral("upnp:album"));
0480     if (!albumNode.isNull()) {
0481         childData.setAlbum(albumNode.toElement().text());
0482     }
0483 
0484     const QDomNode &albumArtNode = itemNode.firstChildElement(QStringLiteral("upnp:albumArtURI"));
0485     if (!albumArtNode.isNull()) {
0486         childData[DataTypes::ColumnsRoles::ImageUrlRole] = QUrl::fromUserInput(albumArtNode.toElement().text());
0487     }
0488 
0489     const QDomNode &resourceNode = itemNode.firstChildElement(QStringLiteral("res"));
0490     if (!resourceNode.isNull()) {
0491         childData[DataTypes::ColumnsRoles::ResourceRole] = QUrl::fromUserInput(resourceNode.toElement().text());
0492         if (resourceNode.attributes().contains(QStringLiteral("duration"))) {
0493             const QDomNode &durationNode = resourceNode.attributes().namedItem(QStringLiteral("duration"));
0494             QString durationValue = durationNode.nodeValue();
0495             if (durationValue.startsWith(QLatin1String("0:"))) {
0496                 durationValue.remove(0, 2);
0497             }
0498             if (durationValue.contains(uint('.'))) {
0499                 durationValue = durationValue.split(QLatin1Char('.')).first();
0500             }
0501 
0502             childData[DataTypes::ColumnsRoles::DurationRole] = QTime::fromString(durationValue, QStringLiteral("mm:ss"));
0503             if (!childData.duration().isValid()) {
0504                 childData[DataTypes::ColumnsRoles::DurationRole] = QTime::fromString(durationValue, QStringLiteral("hh:mm:ss"));
0505                 if (!childData.duration().isValid()) {
0506                     childData[DataTypes::ColumnsRoles::DurationRole] = QTime::fromString(durationValue, QStringLiteral("hh:mm:ss.z"));
0507                 }
0508             }
0509         }
0510 
0511         const QDomNode &trackNumberNode = itemNode.firstChildElement(QStringLiteral("upnp:originalTrackNumber"));
0512         if (!trackNumberNode.isNull()) {
0513             childData[DataTypes::ColumnsRoles::TrackNumberRole] = trackNumberNode.toElement().text().toInt();
0514         }
0515 
0516         if (resourceNode.attributes().contains(QStringLiteral("artist"))) {
0517             const QDomNode &artistNode = resourceNode.attributes().namedItem(QStringLiteral("artist"));
0518             childData[DataTypes::ColumnsRoles::ArtistRole] = artistNode.nodeValue();
0519         }
0520     }
0521 
0522     qCDebug(orgKdeElisaUpnp()) << "DidlParser::decodeAudioTrackNode" << childData;
0523 }
0524 
0525 
0526 #include "moc_didlparser.cpp"
0527