filescanner.cpp 9.48 KB
Newer Older
1
/*
Matthieu Gallien's avatar
Matthieu Gallien committed
2
3
4
   SPDX-FileCopyrightText: 2018 (c) Matthieu Gallien <matthieu_gallien@yahoo.fr>

   SPDX-License-Identifier: LGPL-3.0-or-later
5
6
7
8
9
10
 */

#include "filescanner.h"

#include "config-upnp-qt.h"

11
12
#include "abstractfile/indexercommon.h"

13
14
15
16
17
18
19
#if defined KF5FileMetaData_FOUND && KF5FileMetaData_FOUND

#include <KFileMetaData/ExtractorCollection>
#include <KFileMetaData/Extractor>
#include <KFileMetaData/SimpleExtractionResult>
#include <KFileMetaData/UserMetaData>
#include <KFileMetaData/Properties>
20
#include <KFileMetaData/EmbeddedImageData>
21
22
23
24
25
26
27
28
29
30

#if defined KF5Baloo_FOUND && KF5Baloo_FOUND

#include <Baloo/File>

#endif

#endif

#include <QFileInfo>
31
#include <QLocale>
32
#include <QDir>
33
#include <QHash>
34
#include <QMimeDatabase>
35
36
37
38
39
40
41
42

class FileScannerPrivate
{
public:
#if defined KF5FileMetaData_FOUND && KF5FileMetaData_FOUND
    KFileMetaData::ExtractorCollection mAllExtractors;

    KFileMetaData::PropertyMap mAllProperties;
43

44
    KFileMetaData::EmbeddedImageData mImageScanner;
45
46
#endif

47
    QMimeDatabase mMimeDb;
48

49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#if defined KF5FileMetaData_FOUND && KF5FileMetaData_FOUND
    const QHash<KFileMetaData::Property::Property, DataTypes::ColumnsRoles> propertyTranslation = {
        {KFileMetaData::Property::Artist, DataTypes::ColumnsRoles::ArtistRole},
        {KFileMetaData::Property::AlbumArtist, DataTypes::ColumnsRoles::AlbumArtistRole},
        {KFileMetaData::Property::Genre, DataTypes::ColumnsRoles::GenreRole},
        {KFileMetaData::Property::Composer, DataTypes::ColumnsRoles::ComposerRole},
        {KFileMetaData::Property::Lyricist, DataTypes::ColumnsRoles::LyricistRole},
        {KFileMetaData::Property::Title, DataTypes::ColumnsRoles::TitleRole},
        {KFileMetaData::Property::Album, DataTypes::ColumnsRoles::AlbumRole},
        {KFileMetaData::Property::TrackNumber, DataTypes::ColumnsRoles::TrackNumberRole},
        {KFileMetaData::Property::DiscNumber, DataTypes::ColumnsRoles::DiscNumberRole},
        {KFileMetaData::Property::ReleaseYear, DataTypes::ColumnsRoles::YearRole},
        {KFileMetaData::Property::Lyrics, DataTypes::ColumnsRoles::LyricsRole},
        {KFileMetaData::Property::Comment, DataTypes::ColumnsRoles::CommentRole},
        {KFileMetaData::Property::Rating, DataTypes::ColumnsRoles::RatingRole},
        {KFileMetaData::Property::Channels, DataTypes::ColumnsRoles::ChannelsRole},
        {KFileMetaData::Property::SampleRate, DataTypes::ColumnsRoles::SampleRateRole},
        {KFileMetaData::Property::BitRate, DataTypes::ColumnsRoles::BitRateRole},
        {KFileMetaData::Property::Duration, DataTypes::ColumnsRoles::DurationRole},
    };
#endif

    const QStringList constSearchStrings = {
        QStringLiteral("*[Cc]over*.jpg"),
        QStringLiteral("*[Cc]over*.png"),
        QStringLiteral("*[Ff]older*.jpg"),
        QStringLiteral("*[Ff]older*.png"),
        QStringLiteral("*[Ff]ront*.jpg"),
77
78
79
        QStringLiteral("*[Ff]ront*.png"),
        QStringLiteral("*[Aa]lbumart*.jpg"),
        QStringLiteral("*[Aa]lbumart*.png")
80
    };
81
82
};

83
84
85
86
FileScanner::FileScanner() : d(std::make_unique<FileScannerPrivate>())
{
}

87
88
89
90
91
92
bool FileScanner::shouldScanFile(const QString &scanFile)
{
    const auto &fileMimeType = d->mMimeDb.mimeTypeForFile(scanFile);
    return fileMimeType.name().startsWith(QLatin1String("audio/"));
}

93
94
FileScanner::~FileScanner() = default;

95
DataTypes::TrackDataType FileScanner::scanOneFile(const QUrl &scanFile, const QFileInfo &scanFileInfo)
96
{
97
    DataTypes::TrackDataType newTrack;
98

99
100
101
102
    if (!scanFile.isLocalFile() && !scanFile.scheme().isEmpty()) {
        return newTrack;
    }

103
    const auto &localFileName = scanFile.toLocalFile();
104

105
106
107
    newTrack[DataTypes::FileModificationTime] = scanFileInfo.metadataChangeTime();
    newTrack[DataTypes::ResourceRole] = scanFile;
    newTrack[DataTypes::RatingRole] = 0;
108

109
#if defined KF5FileMetaData_FOUND && KF5FileMetaData_FOUND
110
    const auto &fileMimeType = d->mMimeDb.mimeTypeForFile(localFileName);
Laurent Montel's avatar
Laurent Montel committed
111
    if (!fileMimeType.name().startsWith(QLatin1String("audio/"))) {
112
113
114
        return newTrack;
    }

115
    const auto &mimetype = fileMimeType.name();
116

117
    const QList<KFileMetaData::Extractor*> &exList = d->mAllExtractors.fetchExtractors(mimetype);
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132

    if (exList.isEmpty()) {
        return newTrack;
    }

    KFileMetaData::Extractor* ex = exList.first();
    KFileMetaData::SimpleExtractionResult result(localFileName, mimetype,
                                                 KFileMetaData::ExtractionResult::ExtractMetaData);

    ex->extract(&result);

    d->mAllProperties = result.properties();

    scanProperties(localFileName, newTrack);

133
    qCDebug(orgKdeElisaIndexer()) << "scanOneFile" << scanFile << "using KFileMetaData" << newTrack;
134
#else
135
    Q_UNUSED(scanFile)
136
    Q_UNUSED(scanFileInfo)
137

138
    qCDebug(orgKdeElisaIndexer()) << "scanOneFile" << scanFile << "no metadata provider" << newTrack;
139
#endif
140
141

    return newTrack;
142
143
}

144
145
146
147
148
DataTypes::TrackDataType FileScanner::scanOneFile(const QUrl &scanFile)
{
    if (!scanFile.isLocalFile()){
        return {};
    } else {
149
        const QFileInfo scanFileInfo(scanFile.toLocalFile());
150
151
152
153
154
        return FileScanner::scanOneFile(scanFile, scanFileInfo);
    }
}

DataTypes::TrackDataType FileScanner::scanOneBalooFile(const QUrl &scanFile, const QFileInfo &scanFileInfo)
155
{
156
    DataTypes::TrackDataType newTrack;
157
#if defined KF5Baloo_FOUND && KF5Baloo_FOUND
158
    const auto &localFileName = scanFile.toLocalFile();
159
160
161
162
163
164
165
166
167

    newTrack[DataTypes::FileModificationTime] = scanFileInfo.metadataChangeTime();
    newTrack[DataTypes::ResourceRole] = scanFile;
    newTrack[DataTypes::RatingRole] = 0;

    Baloo::File match(localFileName);

    match.load();

168
    d->mAllProperties = match.properties();
169
    scanProperties(match.path(), newTrack);
170

171
    qCDebug(orgKdeElisaIndexer()) << "scanOneFile" << scanFile << "using Baloo" << newTrack;
172
#else
173
174
    Q_UNUSED(scanFile)
    Q_UNUSED(scanFileInfo)
175

176
    qCDebug(orgKdeElisaIndexer()) << "scanOneFile" << scanFile << "no baloo metadata provider" << newTrack;
177
#endif
178
    return newTrack;
179
180
}

181
void FileScanner::scanProperties(const QString &localFileName, DataTypes::TrackDataType &trackData)
182
183
{
#if defined KF5FileMetaData_FOUND && KF5FileMetaData_FOUND
184
185
    if (d->mAllProperties.isEmpty()) {
        return;
186
    }
187
    using entry = std::pair<const KFileMetaData::Property::Property&, const QVariant&>;
188

189
190
191
    auto rangeBegin = d->mAllProperties.constKeyValueBegin();
    QVariant value;
    while (rangeBegin != d->mAllProperties.constKeyValueEnd()) {
192
        const auto key = (*rangeBegin).first;
193

194
        const auto rangeEnd = std::find_if(rangeBegin, d->mAllProperties.constKeyValueEnd(),
Matthieu Gallien's avatar
Matthieu Gallien committed
195
                                     [key](entry e) { return e.first != key; });
196

197
        const auto distance = std::distance(rangeBegin, rangeEnd);
198
199
200
        if (distance > 1) {
            QStringList list;
            list.reserve(static_cast<int>(distance));
Matthieu Gallien's avatar
Matthieu Gallien committed
201
            std::for_each(rangeBegin, rangeEnd, [&list](entry s) { list.append(s.second.toString()); });
202
203
204
205
            value = QLocale().createSeparatedList(list);
        } else {
            value = (*rangeBegin).second;
        }
206
        const auto &translatedKey = d->propertyTranslation.find(key);
207
208
        if (translatedKey.value() == DataTypes::DurationRole) {
            trackData.insert(translatedKey.value(), QTime::fromMSecsSinceStartOfDay(int(1000 * (*rangeBegin).second.toDouble())));
209
        } else if (translatedKey != d->propertyTranslation.end()) {
210
            trackData.insert(translatedKey.value(), (*rangeBegin).second);
211
212
        }
        rangeBegin = rangeEnd;
213
214
    }

215
216
217
218
219
220
    if (!trackData.isValid()) {
        return;
    }

    trackData[DataTypes::HasEmbeddedCover] = checkEmbeddedCoverImage(localFileName);

221
#if !defined Q_OS_ANDROID && !defined Q_OS_WIN
222
223
    const auto fileData = KFileMetaData::UserMetaData(localFileName);
    const auto &comment = fileData.userComment();
224
    if (!comment.isEmpty()) {
225
        trackData[DataTypes::CommentRole] = comment;
226
227
    }

228
    const auto rating = fileData.rating();
229
    if (rating >= 0) {
230
        trackData[DataTypes::RatingRole] = rating;
231
    }
232
233
234
235
236
237
238
#endif

#else
    Q_UNUSED(localFileName)
    Q_UNUSED(trackData)
#endif
}
239

240
241
QUrl FileScanner::searchForCoverFile(const QString &localFileName)
{
242
    const QFileInfo trackFilePath(localFileName);
243
    QDir trackFileDir = trackFilePath.absoluteDir();
244
    trackFileDir.setFilter(QDir::Files);
245
    trackFileDir.setNameFilters(d->constSearchStrings);
246
    QFileInfoList coverFiles = trackFileDir.entryInfoList();
247
    if (coverFiles.isEmpty()) {
248
249
250
        const QString dirNamePattern = QLatin1String("*") + trackFileDir.dirName() + QLatin1String("*");
        const QString dirNameNoSpaces = QLatin1String("*") + trackFileDir.dirName().remove(QLatin1Char(' ')) + QLatin1String("*");
        const QStringList filters = {
251
252
253
254
255
256
257
258
            dirNamePattern + QStringLiteral(".jpg"),
            dirNamePattern + QStringLiteral(".png"),
            dirNameNoSpaces + QStringLiteral(".jpg"),
            dirNameNoSpaces + QStringLiteral(".png")
        };
        trackFileDir.setNameFilters(filters);
        coverFiles = trackFileDir.entryInfoList();
    }
259
260
261
    if (coverFiles.isEmpty()) {
        return QUrl();
    }
262
    return QUrl::fromLocalFile(coverFiles.first().absoluteFilePath());
263
264
}

265
bool FileScanner::checkEmbeddedCoverImage(const QString &localFileName)
266
267
{
#if defined KF5FileMetaData_FOUND && KF5FileMetaData_FOUND
268
    const auto &imageData = d->mImageScanner.imageData(localFileName);
269
270
271
272
273
274

    if (imageData.contains(KFileMetaData::EmbeddedImageData::FrontCover)) {
        if (!imageData[KFileMetaData::EmbeddedImageData::FrontCover].isEmpty()) {
            return true;
        }
    }
Matthieu Gallien's avatar
Matthieu Gallien committed
275
276
#else
    Q_UNUSED(localFileName)
277
#endif
278
279

    return false;
280
}