File indexing completed on 2024-04-14 04:43:59

0001 /*
0002    SPDX-FileCopyrightText: 2018 (c) Matthieu Gallien <matthieu_gallien@yahoo.fr>
0003 
0004    SPDX-License-Identifier: LGPL-3.0-or-later
0005  */
0006 
0007 #include "filescanner.h"
0008 
0009 #include "config-upnp-qt.h"
0010 
0011 #include "abstractfile/indexercommon.h"
0012 
0013 #if KFFileMetaData_FOUND
0014 
0015 #include <KFileMetaData/ExtractorCollection>
0016 #include <KFileMetaData/Extractor>
0017 #include <KFileMetaData/SimpleExtractionResult>
0018 #include <KFileMetaData/UserMetaData>
0019 #include <KFileMetaData/Properties>
0020 
0021 #endif
0022 
0023 #include <QFileInfo>
0024 #include <QLocale>
0025 #include <QDir>
0026 #include <QHash>
0027 #include <QMimeDatabase>
0028 
0029 QStringList buildCoverFileNames(const QStringList &fileNames, const QStringList &fileExtensions)
0030 {
0031     QStringList covers {};
0032     for (const auto &fileName : fileNames) {
0033         for (const auto &fileExtension : fileExtensions) {
0034             covers.push_back(fileName + fileExtension);
0035         }
0036     }
0037     return covers;
0038 }
0039 
0040 class FileScannerPrivate
0041 {
0042 public:
0043 #if KFFileMetaData_FOUND
0044     KFileMetaData::ExtractorCollection mAllExtractors;
0045 
0046     KFileMetaData::PropertyMultiMap mAllProperties;
0047 
0048     KFileMetaData::EmbeddedImageData mImageScanner;
0049 #endif
0050 
0051     QMimeDatabase mMimeDb;
0052 
0053 #if KFFileMetaData_FOUND
0054     const QHash<KFileMetaData::Property::Property, DataTypes::ColumnsRoles> propertyTranslation = {
0055         {KFileMetaData::Property::Artist, DataTypes::ColumnsRoles::ArtistRole},
0056         {KFileMetaData::Property::AlbumArtist, DataTypes::ColumnsRoles::AlbumArtistRole},
0057         {KFileMetaData::Property::Genre, DataTypes::ColumnsRoles::GenreRole},
0058         {KFileMetaData::Property::Composer, DataTypes::ColumnsRoles::ComposerRole},
0059         {KFileMetaData::Property::Lyricist, DataTypes::ColumnsRoles::LyricistRole},
0060         {KFileMetaData::Property::Title, DataTypes::ColumnsRoles::TitleRole},
0061         {KFileMetaData::Property::Album, DataTypes::ColumnsRoles::AlbumRole},
0062         {KFileMetaData::Property::TrackNumber, DataTypes::ColumnsRoles::TrackNumberRole},
0063         {KFileMetaData::Property::DiscNumber, DataTypes::ColumnsRoles::DiscNumberRole},
0064         {KFileMetaData::Property::ReleaseYear, DataTypes::ColumnsRoles::YearRole},
0065         {KFileMetaData::Property::Lyrics, DataTypes::ColumnsRoles::LyricsRole},
0066         {KFileMetaData::Property::Comment, DataTypes::ColumnsRoles::CommentRole},
0067         {KFileMetaData::Property::Rating, DataTypes::ColumnsRoles::RatingRole},
0068         {KFileMetaData::Property::Channels, DataTypes::ColumnsRoles::ChannelsRole},
0069         {KFileMetaData::Property::SampleRate, DataTypes::ColumnsRoles::SampleRateRole},
0070         {KFileMetaData::Property::BitRate, DataTypes::ColumnsRoles::BitRateRole},
0071         {KFileMetaData::Property::Duration, DataTypes::ColumnsRoles::DurationRole},
0072     };
0073 #endif
0074 
0075     const QStringList constCoverExtensions {
0076         QStringLiteral(".jpg")
0077         ,QStringLiteral(".jpeg")
0078         ,QStringLiteral(".png")
0079         ,QStringLiteral(".webp")
0080     };
0081 
0082     const QStringList constCoverNames {
0083         QStringLiteral("[Cc]over")
0084         ,QStringLiteral("[Cc]over[Ii]mage")
0085         ,QStringLiteral("[Ff]older")
0086         ,QStringLiteral("[Aa]lbumart")
0087     };
0088 
0089     const QStringList constCoverGlobs = {
0090         QStringLiteral("*[Cc]over*")
0091         ,QStringLiteral("*[Ff]older*")
0092         ,QStringLiteral("*[Ff]ront*")
0093         ,QStringLiteral("*[Aa]lbumart*")
0094     };
0095 
0096     const QStringList coverFileAllImages = buildCoverFileNames({QStringLiteral("*")}, constCoverExtensions);
0097     const QStringList coverFileNames = buildCoverFileNames(constCoverNames, constCoverExtensions);
0098     const QStringList coverFileGlobs = buildCoverFileNames(constCoverGlobs, constCoverExtensions);
0099 };
0100 
0101 FileScanner::FileScanner() : d(std::make_unique<FileScannerPrivate>())
0102 {
0103 }
0104 
0105 bool FileScanner::shouldScanFile(const QString &scanFile)
0106 {
0107     const auto &fileMimeType = d->mMimeDb.mimeTypeForFile(scanFile);
0108     return fileMimeType.name().startsWith(QLatin1String("audio/"));
0109 }
0110 
0111 FileScanner::~FileScanner() = default;
0112 
0113 DataTypes::TrackDataType FileScanner::scanOneFile(const QUrl &scanFile, const QFileInfo &scanFileInfo)
0114 {
0115     DataTypes::TrackDataType newTrack;
0116 
0117     if (!scanFile.isLocalFile() && !scanFile.scheme().isEmpty()) {
0118         return newTrack;
0119     }
0120 
0121     const auto &localFileName = scanFile.toLocalFile();
0122 
0123     newTrack[DataTypes::FileModificationTime] = scanFileInfo.metadataChangeTime();
0124     newTrack[DataTypes::ResourceRole] = scanFile;
0125     newTrack[DataTypes::RatingRole] = 0;
0126     newTrack[DataTypes::ElementTypeRole] = ElisaUtils::Track;
0127 
0128 #if KFFileMetaData_FOUND
0129     const auto &fileMimeType = d->mMimeDb.mimeTypeForFile(localFileName);
0130     if (!fileMimeType.name().startsWith(QLatin1String("audio/"))) {
0131         return newTrack;
0132     }
0133 
0134     const auto &mimetype = fileMimeType.name();
0135 
0136     const QList<KFileMetaData::Extractor*> &exList = d->mAllExtractors.fetchExtractors(mimetype);
0137 
0138     if (exList.isEmpty()) {
0139         // when no extractors exist and we have an audio file, we fallback to filling the minimal
0140         // set of properties to let Elisa be able to recognise and play the file.
0141 
0142         qCDebug(orgKdeElisaIndexer()) << "FileScanner::shouldScanFile" << scanFile << localFileName << "no extractors" << fileMimeType;
0143 
0144         newTrack[DataTypes::FileModificationTime] = scanFileInfo.metadataChangeTime();
0145         newTrack[DataTypes::ResourceRole] = scanFile;
0146         newTrack[DataTypes::RatingRole] = 0;
0147         newTrack[DataTypes::DurationRole] = QTime::fromMSecsSinceStartOfDay(1);
0148         newTrack[DataTypes::ElementTypeRole] = ElisaUtils::Track;
0149 
0150         return newTrack;
0151     }
0152 
0153     KFileMetaData::Extractor* ex = exList.first();
0154     KFileMetaData::SimpleExtractionResult result(localFileName, mimetype,
0155                                                  KFileMetaData::ExtractionResult::ExtractMetaData);
0156 
0157     ex->extract(&result);
0158 
0159     d->mAllProperties = result.properties();
0160 
0161     scanProperties(localFileName, newTrack);
0162 
0163     qCDebug(orgKdeElisaIndexer()) << "scanOneFile" << scanFile << "using KFileMetaData" << newTrack;
0164 #else
0165     Q_UNUSED(scanFile)
0166     Q_UNUSED(scanFileInfo)
0167 
0168     qCDebug(orgKdeElisaIndexer()) << "scanOneFile" << scanFile << "no metadata provider" << newTrack;
0169 #endif
0170 
0171     return newTrack;
0172 }
0173 
0174 DataTypes::TrackDataType FileScanner::scanOneFile(const QUrl &scanFile)
0175 {
0176     if (!scanFile.isLocalFile()){
0177         return {};
0178     } else {
0179         const QFileInfo scanFileInfo(scanFile.toLocalFile());
0180         return FileScanner::scanOneFile(scanFile, scanFileInfo);
0181     }
0182 }
0183 
0184 void FileScanner::scanProperties(const QString &localFileName, DataTypes::TrackDataType &trackData)
0185 {
0186 #if KFFileMetaData_FOUND
0187     if (d->mAllProperties.isEmpty()) {
0188         return;
0189     }
0190     using entry = std::pair<const KFileMetaData::Property::Property&, const QVariant&>;
0191 
0192     auto rangeBegin = d->mAllProperties.constKeyValueBegin();
0193     QVariant value;
0194     while (rangeBegin != d->mAllProperties.constKeyValueEnd()) {
0195         const auto key = (*rangeBegin).first;
0196 
0197         const auto rangeEnd = std::find_if(rangeBegin, d->mAllProperties.constKeyValueEnd(),
0198                                      [key](entry e) { return e.first != key; });
0199 
0200         const auto distance = std::distance(rangeBegin, rangeEnd);
0201         if (distance > 1) {
0202             QStringList list;
0203             list.reserve(static_cast<int>(distance));
0204             std::for_each(rangeBegin, rangeEnd, [&list](entry s) { list.append(s.second.toString()); });
0205             value = QLocale().createSeparatedList(list);
0206         } else {
0207             value = (*rangeBegin).second;
0208         }
0209         if (d->propertyTranslation.contains(key)) {
0210             const auto &translatedKey = d->propertyTranslation.find(key);
0211             if (translatedKey.value() == DataTypes::DurationRole) {
0212                 trackData.insert(translatedKey.value(), QTime::fromMSecsSinceStartOfDay(int(1000 * (*rangeBegin).second.toDouble())));
0213             } else if (translatedKey != d->propertyTranslation.end()) {
0214                 trackData.insert(translatedKey.value(), (*rangeBegin).second);
0215             }
0216         }
0217         rangeBegin = rangeEnd;
0218     }
0219 
0220     if (!trackData.isValid()) {
0221         return;
0222     }
0223 
0224     if (checkEmbeddedCoverImage(localFileName)) {
0225         trackData[DataTypes::HasEmbeddedCover] = true;
0226         trackData[DataTypes::ImageUrlRole] = QUrl(QLatin1String("image://cover/") + localFileName);
0227     } else {
0228         trackData[DataTypes::HasEmbeddedCover] = false;
0229     }
0230 
0231 #if !defined Q_OS_ANDROID && !defined Q_OS_WIN
0232     const auto fileData = KFileMetaData::UserMetaData(localFileName);
0233     const auto &comment = fileData.userComment();
0234     if (!comment.isEmpty()) {
0235         trackData[DataTypes::CommentRole] = comment;
0236     }
0237 
0238     const auto rating = fileData.rating();
0239     if (rating >= 0) {
0240         trackData[DataTypes::RatingRole] = rating;
0241     }
0242 #endif
0243 
0244 #else
0245     Q_UNUSED(localFileName)
0246     Q_UNUSED(trackData)
0247 #endif
0248 }
0249 
0250 QUrl FileScanner::searchForCoverFile(const QString &localFileName)
0251 {
0252     const QFileInfo trackFilePath(localFileName);
0253     QDir trackFileDir = trackFilePath.absoluteDir();
0254 
0255     static QHash<QString, QUrl> directoryCache;
0256     if (directoryCache.contains(trackFileDir.path())) {
0257         return directoryCache.value(trackFileDir.path());
0258     }
0259 
0260     trackFileDir.setFilter(QDir::Files);
0261     trackFileDir.setNameFilters(d->coverFileAllImages);
0262     QFileInfoList coverFiles = trackFileDir.entryInfoList();
0263 
0264     if (coverFiles.isEmpty()) {
0265         directoryCache.insert(trackFileDir.path(), QUrl());
0266         return QUrl();
0267     }
0268 
0269     if (coverFiles.length() != 1) {
0270         trackFileDir.setNameFilters(d->coverFileNames);
0271         coverFiles = trackFileDir.entryInfoList();
0272     }
0273 
0274     if (coverFiles.isEmpty()) {
0275         trackFileDir.setNameFilters(d->coverFileGlobs);
0276         coverFiles = trackFileDir.entryInfoList();
0277     }
0278 
0279     if (coverFiles.isEmpty()) {
0280         const QString dirNamePattern = QLatin1String("*") + trackFileDir.dirName() + QLatin1String("*");
0281         const QString dirNameNoSpaces = QLatin1String("*") + trackFileDir.dirName().remove(QLatin1Char(' ')) + QLatin1String("*");
0282         auto filters = buildCoverFileNames({dirNamePattern, dirNameNoSpaces}, d->constCoverExtensions);
0283         trackFileDir.setNameFilters(filters);
0284         coverFiles = trackFileDir.entryInfoList();
0285     }
0286 
0287     if (coverFiles.isEmpty()) {
0288         trackFileDir.setNameFilters(d->coverFileAllImages);
0289         coverFiles = trackFileDir.entryInfoList();
0290     }
0291 
0292     if (coverFiles.isEmpty()) {
0293         directoryCache.insert(trackFileDir.path(), QUrl());
0294         return QUrl();
0295     }
0296 
0297     const QUrl url = QUrl::fromLocalFile(coverFiles.first().absoluteFilePath());
0298     directoryCache.insert(trackFileDir.path(), url);
0299 
0300     return url;
0301 }
0302 
0303 bool FileScanner::checkEmbeddedCoverImage(const QString &localFileName)
0304 {
0305 #if KFFileMetaData_FOUND
0306     const auto &mimeType = QMimeDatabase().mimeTypeForFile(localFileName).name();
0307     auto extractors = d->mAllExtractors.fetchExtractors(mimeType);
0308 
0309     for (const auto &extractor : extractors) {
0310         KFileMetaData::SimpleExtractionResult result(localFileName, mimeType, KFileMetaData::ExtractionResult::ExtractImageData);
0311         extractor->extract(&result);
0312         if (!result.imageData().isEmpty()) {
0313             return true;
0314         }
0315     }
0316 
0317 #else
0318     Q_UNUSED(localFileName)
0319 #endif
0320 
0321     return false;
0322 }