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 }