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