1 /*
2    SPDX-FileCopyrightText: 2018 (c) Matthieu Gallien <matthieu_gallien@yahoo.fr>
3 
4    SPDX-License-Identifier: LGPL-3.0-or-later
5  */
6 
7 #include "filescanner.h"
8 
9 #include "config-upnp-qt.h"
10 
11 #include "abstractfile/indexercommon.h"
12 
13 #if defined KF5FileMetaData_FOUND && KF5FileMetaData_FOUND
14 
15 #include <KFileMetaData/ExtractorCollection>
16 #include <KFileMetaData/Extractor>
17 #include <KFileMetaData/SimpleExtractionResult>
18 #include <KFileMetaData/UserMetaData>
19 #include <KFileMetaData/Properties>
20 #include <KFileMetaData/EmbeddedImageData>
21 
22 #if defined KF5Baloo_FOUND && KF5Baloo_FOUND
23 
24 #include <Baloo/File>
25 
26 #endif
27 
28 #endif
29 
30 #include <QFileInfo>
31 #include <QLocale>
32 #include <QDir>
33 #include <QHash>
34 #include <QMimeDatabase>
35 
36 class FileScannerPrivate
37 {
38 public:
39 #if defined KF5FileMetaData_FOUND && KF5FileMetaData_FOUND
40     KFileMetaData::ExtractorCollection mAllExtractors;
41 
42     KFileMetaData::PropertyMap mAllProperties;
43 
44     KFileMetaData::EmbeddedImageData mImageScanner;
45 #endif
46 
47     QMimeDatabase mMimeDb;
48 
49 #if defined KF5FileMetaData_FOUND && KF5FileMetaData_FOUND
50     const QHash<KFileMetaData::Property::Property, DataTypes::ColumnsRoles> propertyTranslation = {
51         {KFileMetaData::Property::Artist, DataTypes::ColumnsRoles::ArtistRole},
52         {KFileMetaData::Property::AlbumArtist, DataTypes::ColumnsRoles::AlbumArtistRole},
53         {KFileMetaData::Property::Genre, DataTypes::ColumnsRoles::GenreRole},
54         {KFileMetaData::Property::Composer, DataTypes::ColumnsRoles::ComposerRole},
55         {KFileMetaData::Property::Lyricist, DataTypes::ColumnsRoles::LyricistRole},
56         {KFileMetaData::Property::Title, DataTypes::ColumnsRoles::TitleRole},
57         {KFileMetaData::Property::Album, DataTypes::ColumnsRoles::AlbumRole},
58         {KFileMetaData::Property::TrackNumber, DataTypes::ColumnsRoles::TrackNumberRole},
59         {KFileMetaData::Property::DiscNumber, DataTypes::ColumnsRoles::DiscNumberRole},
60         {KFileMetaData::Property::ReleaseYear, DataTypes::ColumnsRoles::YearRole},
61         {KFileMetaData::Property::Lyrics, DataTypes::ColumnsRoles::LyricsRole},
62         {KFileMetaData::Property::Comment, DataTypes::ColumnsRoles::CommentRole},
63         {KFileMetaData::Property::Rating, DataTypes::ColumnsRoles::RatingRole},
64         {KFileMetaData::Property::Channels, DataTypes::ColumnsRoles::ChannelsRole},
65         {KFileMetaData::Property::SampleRate, DataTypes::ColumnsRoles::SampleRateRole},
66         {KFileMetaData::Property::BitRate, DataTypes::ColumnsRoles::BitRateRole},
67         {KFileMetaData::Property::Duration, DataTypes::ColumnsRoles::DurationRole},
68     };
69 #endif
70 
71     const QStringList constSearchStrings = {
72         QStringLiteral("*[Cc]over*.jpg")
73         ,QStringLiteral("*[Cc]over*.png")
74         ,QStringLiteral("*[Ff]older*.jpg")
75         ,QStringLiteral("*[Ff]older*.png")
76         ,QStringLiteral("*[Ff]ront*.jpg")
77         ,QStringLiteral("*[Ff]ront*.png")
78         ,QStringLiteral("*[Aa]lbumart*.jpg")
79         ,QStringLiteral("*[Aa]lbumart*.png")
80         ,QStringLiteral("*[Cc]over*.jpg")
81         ,QStringLiteral("*[Cc]over*.png")
82     };
83 };
84 
FileScanner()85 FileScanner::FileScanner() : d(std::make_unique<FileScannerPrivate>())
86 {
87 }
88 
shouldScanFile(const QString & scanFile)89 bool FileScanner::shouldScanFile(const QString &scanFile)
90 {
91     const auto &fileMimeType = d->mMimeDb.mimeTypeForFile(scanFile);
92     return fileMimeType.name().startsWith(QLatin1String("audio/"));
93 }
94 
95 FileScanner::~FileScanner() = default;
96 
scanOneFile(const QUrl & scanFile,const QFileInfo & scanFileInfo)97 DataTypes::TrackDataType FileScanner::scanOneFile(const QUrl &scanFile, const QFileInfo &scanFileInfo)
98 {
99     DataTypes::TrackDataType newTrack;
100 
101     if (!scanFile.isLocalFile() && !scanFile.scheme().isEmpty()) {
102         return newTrack;
103     }
104 
105     const auto &localFileName = scanFile.toLocalFile();
106 
107     newTrack[DataTypes::FileModificationTime] = scanFileInfo.metadataChangeTime();
108     newTrack[DataTypes::ResourceRole] = scanFile;
109     newTrack[DataTypes::RatingRole] = 0;
110     newTrack[DataTypes::ElementTypeRole] = ElisaUtils::Track;
111 
112 #if defined KF5FileMetaData_FOUND && KF5FileMetaData_FOUND
113     const auto &fileMimeType = d->mMimeDb.mimeTypeForFile(localFileName);
114     if (!fileMimeType.name().startsWith(QLatin1String("audio/"))) {
115         return newTrack;
116     }
117 
118     const auto &mimetype = fileMimeType.name();
119 
120     const QList<KFileMetaData::Extractor*> &exList = d->mAllExtractors.fetchExtractors(mimetype);
121 
122     if (exList.isEmpty()) {
123         // when no extractors exist and we have an audio file, we fallback to filling the minimal
124         // set of properties to let Elisa be able to recognise and play the file.
125 
126         qCDebug(orgKdeElisaIndexer()) << "FileScanner::shouldScanFile" << scanFile << localFileName << "no extractors" << fileMimeType;
127 
128         newTrack[DataTypes::FileModificationTime] = scanFileInfo.metadataChangeTime();
129         newTrack[DataTypes::ResourceRole] = scanFile;
130         newTrack[DataTypes::RatingRole] = 0;
131         newTrack[DataTypes::DurationRole] = QTime::fromMSecsSinceStartOfDay(1);
132         newTrack[DataTypes::ElementTypeRole] = ElisaUtils::Track;
133 
134         return newTrack;
135     }
136 
137     KFileMetaData::Extractor* ex = exList.first();
138     KFileMetaData::SimpleExtractionResult result(localFileName, mimetype,
139                                                  KFileMetaData::ExtractionResult::ExtractMetaData);
140 
141     ex->extract(&result);
142 
143     d->mAllProperties = result.properties();
144 
145     scanProperties(localFileName, newTrack);
146 
147     qCDebug(orgKdeElisaIndexer()) << "scanOneFile" << scanFile << "using KFileMetaData" << newTrack;
148 #else
149     Q_UNUSED(scanFile)
150     Q_UNUSED(scanFileInfo)
151 
152     qCDebug(orgKdeElisaIndexer()) << "scanOneFile" << scanFile << "no metadata provider" << newTrack;
153 #endif
154 
155     return newTrack;
156 }
157 
scanOneFile(const QUrl & scanFile)158 DataTypes::TrackDataType FileScanner::scanOneFile(const QUrl &scanFile)
159 {
160     if (!scanFile.isLocalFile()){
161         return {};
162     } else {
163         const QFileInfo scanFileInfo(scanFile.toLocalFile());
164         return FileScanner::scanOneFile(scanFile, scanFileInfo);
165     }
166 }
167 
scanOneBalooFile(const QUrl & scanFile,const QFileInfo & scanFileInfo)168 DataTypes::TrackDataType FileScanner::scanOneBalooFile(const QUrl &scanFile, const QFileInfo &scanFileInfo)
169 {
170     DataTypes::TrackDataType newTrack;
171 #if defined KF5Baloo_FOUND && KF5Baloo_FOUND
172     const auto &localFileName = scanFile.toLocalFile();
173 
174     newTrack[DataTypes::FileModificationTime] = scanFileInfo.metadataChangeTime();
175     newTrack[DataTypes::ResourceRole] = scanFile;
176     newTrack[DataTypes::RatingRole] = 0;
177     newTrack[DataTypes::ElementTypeRole] = ElisaUtils::Track;
178 
179     Baloo::File match(localFileName);
180 
181     match.load();
182 
183     d->mAllProperties = match.properties();
184     scanProperties(match.path(), newTrack);
185 
186     qCDebug(orgKdeElisaIndexer()) << "scanOneFile" << scanFile << "using Baloo" << newTrack;
187 #else
188     Q_UNUSED(scanFile)
189     Q_UNUSED(scanFileInfo)
190 
191     qCDebug(orgKdeElisaIndexer()) << "scanOneFile" << scanFile << "no baloo metadata provider" << newTrack;
192 #endif
193     return newTrack;
194 }
195 
scanProperties(const QString & localFileName,DataTypes::TrackDataType & trackData)196 void FileScanner::scanProperties(const QString &localFileName, DataTypes::TrackDataType &trackData)
197 {
198 #if defined KF5FileMetaData_FOUND && KF5FileMetaData_FOUND
199     if (d->mAllProperties.isEmpty()) {
200         return;
201     }
202     using entry = std::pair<const KFileMetaData::Property::Property&, const QVariant&>;
203 
204     auto rangeBegin = d->mAllProperties.constKeyValueBegin();
205     QVariant value;
206     while (rangeBegin != d->mAllProperties.constKeyValueEnd()) {
207         const auto key = (*rangeBegin).first;
208 
209         const auto rangeEnd = std::find_if(rangeBegin, d->mAllProperties.constKeyValueEnd(),
210                                      [key](entry e) { return e.first != key; });
211 
212         const auto distance = std::distance(rangeBegin, rangeEnd);
213         if (distance > 1) {
214             QStringList list;
215             list.reserve(static_cast<int>(distance));
216             std::for_each(rangeBegin, rangeEnd, [&list](entry s) { list.append(s.second.toString()); });
217             value = QLocale().createSeparatedList(list);
218         } else {
219             value = (*rangeBegin).second;
220         }
221         const auto &translatedKey = d->propertyTranslation.find(key);
222         if (translatedKey.value() == DataTypes::DurationRole) {
223             trackData.insert(translatedKey.value(), QTime::fromMSecsSinceStartOfDay(int(1000 * (*rangeBegin).second.toDouble())));
224         } else if (translatedKey != d->propertyTranslation.end()) {
225             trackData.insert(translatedKey.value(), (*rangeBegin).second);
226         }
227         rangeBegin = rangeEnd;
228     }
229 
230     if (!trackData.isValid()) {
231         return;
232     }
233 
234     trackData[DataTypes::HasEmbeddedCover] = checkEmbeddedCoverImage(localFileName);
235 
236 #if !defined Q_OS_ANDROID && !defined Q_OS_WIN
237     const auto fileData = KFileMetaData::UserMetaData(localFileName);
238     const auto &comment = fileData.userComment();
239     if (!comment.isEmpty()) {
240         trackData[DataTypes::CommentRole] = comment;
241     }
242 
243     const auto rating = fileData.rating();
244     if (rating >= 0) {
245         trackData[DataTypes::RatingRole] = rating;
246     }
247 #endif
248 
249 #else
250     Q_UNUSED(localFileName)
251     Q_UNUSED(trackData)
252 #endif
253 }
254 
searchForCoverFile(const QString & localFileName)255 QUrl FileScanner::searchForCoverFile(const QString &localFileName)
256 {
257     const QFileInfo trackFilePath(localFileName);
258     QDir trackFileDir = trackFilePath.absoluteDir();
259     trackFileDir.setFilter(QDir::Files);
260     trackFileDir.setNameFilters(d->constSearchStrings);
261     QFileInfoList coverFiles = trackFileDir.entryInfoList();
262     if (coverFiles.isEmpty()) {
263         const QString dirNamePattern = QLatin1String("*") + trackFileDir.dirName() + QLatin1String("*");
264         const QString dirNameNoSpaces = QLatin1String("*") + trackFileDir.dirName().remove(QLatin1Char(' ')) + QLatin1String("*");
265         const QStringList filters = {
266             dirNamePattern + QStringLiteral(".jpg"),
267             dirNamePattern + QStringLiteral(".png"),
268             dirNameNoSpaces + QStringLiteral(".jpg"),
269             dirNameNoSpaces + QStringLiteral(".png")
270         };
271         trackFileDir.setNameFilters(filters);
272         coverFiles = trackFileDir.entryInfoList();
273     }
274     if (coverFiles.isEmpty()) {
275         return QUrl();
276     }
277     return QUrl::fromLocalFile(coverFiles.first().absoluteFilePath());
278 }
279 
checkEmbeddedCoverImage(const QString & localFileName)280 bool FileScanner::checkEmbeddedCoverImage(const QString &localFileName)
281 {
282 #if defined KF5FileMetaData_FOUND && KF5FileMetaData_FOUND
283     const auto &imageData = d->mImageScanner.imageData(localFileName);
284 
285     if (imageData.contains(KFileMetaData::EmbeddedImageData::FrontCover)) {
286         if (!imageData[KFileMetaData::EmbeddedImageData::FrontCover].isEmpty()) {
287             return true;
288         }
289     }
290 #else
291     Q_UNUSED(localFileName)
292 #endif
293 
294     return false;
295 }
296