Baloo Widgets

filemetadataprovider.cpp
1/*
2 SPDX-FileCopyrightText: 2010 Peter Penz <peter.penz@gmx.at>
3 SPDX-FileCopyrightText: 2012 Vishesh Handa <me@vhanda.in>
4 SPDX-FileCopyrightText: 2021 Kai Uwe Broulik <kde@broulik.de>
5
6 SPDX-License-Identifier: LGPL-2.0-or-later
7*/
8
9#include "filemetadataprovider.h"
10#include "filemetadatautil_p.h"
11#include "filefetchjob.h"
12
13#include <Baloo/IndexerConfig>
14#include <KFileMetaData/PropertyInfo>
15#include <KFormat>
16#include <KLocalizedString>
17#include <KProtocolInfo>
18#include <KShell>
19
20#include <QPair>
21#include <QSize>
22
23// Required includes for subDirectoriesCount():
24#ifdef Q_OS_WIN
25#include <QDir>
26#else
27#include <QFile>
28#include <dirent.h>
29#endif
30
31using namespace Baloo;
32
33namespace
34{
35/**
36 * The standard QMap::unite will contain the key multiple times if both \p v1 and \p v2
37 * contain the same key.
38 *
39 * This will only take the key from \p v2 into account
40 */
41QVariantMap unite(const QVariantMap &v1, const QVariantMap &v2)
42{
43 QVariantMap v(v1);
45 while (it.hasNext()) {
46 it.next();
47
48 v[it.key()] = it.value();
49 }
50
51 return v;
52}
53
54/**
55* @return The number of files and hidden files for the directory path.
56*/
57QPair<int, int> subDirectoriesCount(const QString &path)
58{
59#ifdef Q_OS_WIN
60 QDir dir(path);
61 int count = dir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::System).count();
62 int hiddenCount = dir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::System | QDir::Hidden).count();
63 return QPair<int, int>(count, hiddenCount);
64#else
65 // Taken from kdelibs/kio/kio/kdirmodel.cpp
66 // SPDX-FileCopyrightText: 2006 David Faure <faure@kde.org>
67
68 int count = -1;
69 int hiddenCount = -1;
70 DIR *dir = ::opendir(QFile::encodeName(path).constData());
71 if (dir) {
72 count = 0;
73 hiddenCount = 0;
74 struct dirent *dirEntry = nullptr;
75 while ((dirEntry = ::readdir(dir))) { // krazy:exclude=syscalls
76 if (dirEntry->d_name[0] == '.') {
77 if (dirEntry->d_name[1] == '\0') {
78 // Skip "."
79 continue;
80 }
81 if (dirEntry->d_name[1] == '.' && dirEntry->d_name[2] == '\0') {
82 // Skip ".."
83 continue;
84 }
85 // hidden files
86 hiddenCount++;
87 } else {
88 ++count;
89 }
90 }
91 ::closedir(dir);
92 }
93 return QPair<int, int>(count, hiddenCount);
94#endif
95}
96
97/**
98 * Fill \p data with properties can be derived from others
99 */
100void extractDerivedProperties(QVariantMap &data)
101{
102 const auto width = data.value(QStringLiteral("width"));
103 const auto height = data.value(QStringLiteral("height"));
104 if (!width.isNull() && !height.isNull()) {
105 data.insert(QStringLiteral("dimensions"), QSize(width.toInt(), height.toInt()));
106 }
107
108 bool okLatitude;
109 const auto gpsLatitude = data.value(QStringLiteral("photoGpsLatitude")).toFloat(&okLatitude);
110 bool okLongitude;
111 const auto gpsLongitude = data.value(QStringLiteral("photoGpsLongitude")).toFloat(&okLongitude);
112
113 if (okLatitude && okLongitude) {
114 data.insert(QStringLiteral("gpsLocation"), QVariant::fromValue(QPair<float, float>(gpsLatitude, gpsLongitude)));
115 }
116}
117} // anonymous namespace
118
119class Q_DECL_HIDDEN Baloo::FileMetaDataProviderPrivate : public QObject
120{
121public:
122 FileMetaDataProviderPrivate(FileMetaDataProvider *parent)
123 : QObject(parent)
124 , m_parent(parent)
125 , m_readOnly(false)
126 {
127 }
128
129 ~FileMetaDataProviderPrivate()
130 {
131 }
132
133 void insertEditableData();
134
135 void processFileItems();
136
137 void setFileItem();
138 void setFileItems();
139
140 /**
141 * Insert basic data of a single file
142 */
143 void insertSingleFileBasicData();
144
145 /**
146 * Insert basic data of a list of files
147 */
148 void insertFilesListBasicData();
149
150 FileMetaDataProvider *m_parent;
151
152 bool m_readOnly;
153
154 QList<KFileItem> m_fileItems;
155
156 QVariantMap m_data;
157 Baloo::IndexerConfig m_config;
158
159public Q_SLOTS:
160 void slotFileFetchFinished(KJob *job);
161};
162
163void FileMetaDataProviderPrivate::slotFileFetchFinished(KJob *job)
164{
165 auto fetchJob = static_cast<FileFetchJob *>(job);
166 QList<QVariantMap> files = fetchJob->data();
167
168 Q_ASSERT(!files.isEmpty());
169
170 if (files.size() > 1) {
171 Baloo::Private::mergeCommonData(m_data, files);
172 } else {
173 m_data = unite(m_data, files.first());
174 }
175 extractDerivedProperties(m_data);
176 m_readOnly = !fetchJob->canEditAll();
177
178 insertEditableData();
179 Q_EMIT m_parent->loadingFinished();
180}
181
182void FileMetaDataProviderPrivate::insertSingleFileBasicData()
183{
184 // TODO: Handle case if remote URLs are used properly. isDir() does
185 // not work, the modification date needs also to be adjusted...
186 Q_ASSERT(m_fileItems.count() == 1);
187 {
188 const KFileItem &item = m_fileItems.first();
189
190 KFormat format;
191 if (item.isDir()) {
192 if (item.isLocalFile() && !item.isSlow()) {
193 const QPair<int, int> counts = subDirectoriesCount(item.url().path());
194 const int count = counts.first;
195 if (count != -1) {
196 QString itemCountString = i18ncp("@item:intable", "%1 item", "%1 items", count);
197 m_data.insert(QStringLiteral("kfileitem#size"), itemCountString);
198
199 const int hiddenCount = counts.second;
200 if (hiddenCount > 0) {
201 // add hidden items count
202 QString hiddenCountString = i18ncp("@item:intable", "%1 item", "%1 items", hiddenCount);
203 m_data.insert(QStringLiteral("kfileitem#hiddenItems"), hiddenCountString);
204 }
205 }
206 } else if (item.entry().contains(KIO::UDSEntry::UDS_SIZE)) {
207 m_data.insert(QStringLiteral("kfileitem#size"), format.formatByteSize(item.size()));
208 }
210 m_data.insert(QStringLiteral("kfileitem#totalSize"), format.formatByteSize(item.recursiveSize()));
211 }
212 } else {
214 m_data.insert(QStringLiteral("kfileitem#size"), format.formatByteSize(item.size()));
215 }
216 }
217
218 m_data.insert(QStringLiteral("kfileitem#type"), item.mimeComment());
219 if (item.isLink()) {
220 m_data.insert(QStringLiteral("kfileitem#linkDest"), item.linkDest());
221 }
223 m_data.insert(QStringLiteral("kfileitem#targetUrl"), KShell::tildeCollapse(item.targetUrl().toDisplayString(QUrl::PreferLocalFile)));
224 }
225 QDateTime modificationTime = item.time(KFileItem::ModificationTime);
226 if (modificationTime.isValid()) {
227 m_data.insert(QStringLiteral("kfileitem#modified"), modificationTime);
228 }
229 QDateTime creationTime = item.time(KFileItem::CreationTime);
230 if (creationTime.isValid()) {
231 m_data.insert(QStringLiteral("kfileitem#created"), creationTime);
232 }
233 QDateTime accessTime = item.time(KFileItem::AccessTime);
234 if (accessTime.isValid()) {
235 m_data.insert(QStringLiteral("kfileitem#accessed"), accessTime);
236 }
237
238 m_data.insert(QStringLiteral("kfileitem#owner"), item.user());
239 m_data.insert(QStringLiteral("kfileitem#group"), item.group());
240 m_data.insert(QStringLiteral("kfileitem#permissions"), item.permissionsString());
241
242 const auto extraFields = KProtocolInfo::extraFields(item.url());
243 for (int i = 0; i < extraFields.count(); ++i) {
244 const auto &field = extraFields.at(i);
245 if (field.type == KProtocolInfo::ExtraField::Invalid) {
246 continue;
247 }
248
249 const QString text = item.entry().stringValue(KIO::UDSEntry::UDS_EXTRA + i);
250 if (text.isEmpty()) {
251 continue;
252 }
253
254 const QString key = QStringLiteral("kfileitem#extra_%1_%2").arg(item.url().scheme(), QString::number(i + 1));
255
256 if (field.type == KProtocolInfo::ExtraField::DateTime) {
257 const QDateTime date = QDateTime::fromString(text, Qt::ISODate);
258 if (!date.isValid()) {
259 continue;
260 }
261
262 m_data.insert(key, date);
263 } else {
264 m_data.insert(key, text);
265 }
266 }
267 }
268}
269
270void FileMetaDataProviderPrivate::insertFilesListBasicData()
271{
272 // If all directories
273 Q_ASSERT(m_fileItems.count() > 1);
274 bool allDirectories = true;
275 for (const KFileItem &item : std::as_const(m_fileItems)) {
276 allDirectories &= item.isDir();
277 if (!allDirectories) {
278 break;
279 }
280 }
281
282 if (allDirectories) {
283 int count = 0;
284 int hiddenCount = 0;
285 for (const KFileItem &item : std::as_const(m_fileItems)) {
286 if (!item.isLocalFile() || item.isSlow()) {
287 return;
288 }
289 const QPair<int, int> counts = subDirectoriesCount(item.url().path());
290 const int subcount = counts.first;
291 if (subcount == -1) {
292 return;
293 }
294 count += subcount;
295 hiddenCount += counts.second;
296 }
297 QString itemCountString = i18ncp("@item:intable", "%1 item", "%1 items", count);
298 if (hiddenCount > 0) {
299 // add hidden items count
300 QString hiddenCountString = i18ncp("@item:intable", "%1 item", "%1 items", hiddenCount);
301 m_data.insert(QStringLiteral("kfileitem#hiddenItems"), hiddenCountString);
302 }
303 m_data.insert(QStringLiteral("kfileitem#totalSize"), itemCountString);
304
305 } else {
306 // Calculate the size of all items
307 quint64 totalSize = 0;
308 for (const KFileItem &item : std::as_const(m_fileItems)) {
309 if (!item.isDir() && !item.isLink()) {
310 totalSize += item.size();
311 }
312 }
313 KFormat format;
314 m_data.insert(QStringLiteral("kfileitem#totalSize"), format.formatByteSize(totalSize));
315 }
316}
317
318void FileMetaDataProviderPrivate::insertEditableData()
319{
320 if (!m_readOnly) {
321 if (!m_data.contains(QStringLiteral("tags"))) {
322 m_data.insert(QStringLiteral("tags"), QVariant());
323 }
324 if (!m_data.contains(QStringLiteral("rating"))) {
325 m_data.insert(QStringLiteral("rating"), 0);
326 }
327 if (!m_data.contains(QStringLiteral("userComment"))) {
328 m_data.insert(QStringLiteral("userComment"), QVariant());
329 }
330 }
331}
332
333FileMetaDataProvider::FileMetaDataProvider(QObject *parent)
334 : QObject(parent)
335 , d(new FileMetaDataProviderPrivate(this))
336{
337}
338
339FileMetaDataProvider::~FileMetaDataProvider() = default;
340
341void FileMetaDataProviderPrivate::processFileItems()
342{
343 // There are several code paths -
344 // Remote file
345 // Single local file -
346 // * Not Indexed
347 // * Indexed
348 //
349 // Multiple Files -
350 // * Not Indexed
351 // * Indexed
352
353 bool singleFileMode = m_fileItems.size() <= 1;
354
355 QStringList urls;
356 urls.reserve(m_fileItems.size());
357 // Only extract data from indexed files,
358 // it would be too expensive otherwise.
359 for (const KFileItem &item : std::as_const(m_fileItems)) {
360 const QUrl url = item.targetUrl();
361 if (url.isLocalFile() && !item.isSlow()) {
362 urls << url.toLocalFile();
363 }
364 }
365
366 if (singleFileMode) {
367 insertSingleFileBasicData();
368 } else {
369 insertFilesListBasicData();
370 }
371
372 if (!urls.isEmpty()) {
373 // Editing only if all URLs are local
374 bool canEdit = (urls.size() == m_fileItems.size());
375
376 // Don't use indexing when we have multiple files
377 auto indexingMode = FileFetchJob::UseRealtimeIndexing::Disabled;
378
379 if (singleFileMode) {
380 // Fully indexed by Baloo
381 indexingMode = FileFetchJob::UseRealtimeIndexing::Fallback;
382
383 if (!m_config.fileIndexingEnabled() || !m_config.shouldBeIndexed(urls.first()) || m_config.onlyBasicIndexing()) {
384 // Not indexed or only basic file indexing (no content)
385 indexingMode = FileFetchJob::UseRealtimeIndexing::Only;
386 }
387 }
388
389 auto job = new FileFetchJob(urls, canEdit, indexingMode, this);
390 connect(job, &FileFetchJob::finished, this, &FileMetaDataProviderPrivate::slotFileFetchFinished);
391 job->start();
392
393 } else {
394 // FIXME - are extended attributes supported for remote files?
395 m_readOnly = true;
396 Q_EMIT m_parent->loadingFinished();
397 }
398}
399
401{
402 d->m_fileItems = items;
403 d->m_data.clear();
404
405 if (items.isEmpty()) {
407 } else {
408 d->processFileItems();
409 }
410}
411
412QString FileMetaDataProvider::label(const QString &metaDataLabel) const
413{
414 static QHash<QString, QString> hash = {
415 {QStringLiteral("kfileitem#comment"), i18nc("@label", "Comment")},
416 {QStringLiteral("kfileitem#created"), i18nc("@label", "Created")},
417 {QStringLiteral("kfileitem#accessed"), i18nc("@label", "Accessed")},
418 {QStringLiteral("kfileitem#modified"), i18nc("@label", "Modified")},
419 {QStringLiteral("kfileitem#owner"), i18nc("@label", "Owner")},
420 {QStringLiteral("kfileitem#group"), i18nc("@label", "Group")},
421 {QStringLiteral("kfileitem#permissions"), i18nc("@label", "Permissions")},
422 {QStringLiteral("kfileitem#rating"), i18nc("@label", "Rating")},
423 {QStringLiteral("kfileitem#size"), i18nc("@label", "Size")},
424 {QStringLiteral("kfileitem#tags"), i18nc("@label", "Tags")},
425 {QStringLiteral("kfileitem#totalSize"), i18nc("@label", "Total Size")},
426 {QStringLiteral("kfileitem#hiddenItems"), i18nc("@label", "Hidden items")},
427 {QStringLiteral("kfileitem#type"), i18nc("@label", "Type")},
428 {QStringLiteral("kfileitem#linkDest"), i18nc("@label", "Link to")},
429 {QStringLiteral("kfileitem#targetUrl"), i18nc("@label", "Points to")},
430 {QStringLiteral("tags"), i18nc("@label", "Tags")},
431 {QStringLiteral("rating"), i18nc("@label", "Rating")},
432 {QStringLiteral("userComment"), i18nc("@label", "Comment")},
433 {QStringLiteral("originUrl"), i18nc("@label", "Downloaded From")},
434 {QStringLiteral("dimensions"), i18nc("@label", "Dimensions")},
435 {QStringLiteral("gpsLocation"), i18nc("@label", "GPS Location")},
436 };
437
438 QString value = hash.value(metaDataLabel);
439 if (value.isEmpty()) {
440 static const auto extraPrefix = QStringLiteral("kfileitem#extra_");
441 if (metaDataLabel.startsWith(extraPrefix)) {
442#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
443 const auto parts = metaDataLabel.splitRef(QLatin1Char('_'));
444#else
445 const auto parts = metaDataLabel.split(QLatin1Char('_'));
446#endif
447 Q_ASSERT(parts.count() == 3);
448 const auto protocol = parts.at(1);
449 const int extraNumber = parts.at(2).toInt() - 1;
450
451 // Have to construct a dummy URL for KProtocolInfo::extraFields...
452 QUrl url;
453#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
454 url.setScheme(protocol.toString());
455#else
456 url.setScheme(protocol);
457#endif
458 const auto extraFields = KProtocolInfo::extraFields(url);
459 auto field = extraFields.value(extraNumber);
460 if (field.type != KProtocolInfo::ExtraField::Invalid) {
461 value = field.name;
462 }
463 }
464 }
465
466 if (value.isEmpty()) {
467 value = KFileMetaData::PropertyInfo::fromName(metaDataLabel).displayName();
468 }
469
470 return value;
471}
472
474{
475 static QHash<QString, QString> uriGrouper = {
476
477 // KFileItem Data
478 {QStringLiteral("kfileitem#type"), QStringLiteral("0FileItemA")},
479 {QStringLiteral("kfileitem#linkDest"), QStringLiteral("0FileItemB")},
480 {QStringLiteral("kfileitem#size"), QStringLiteral("0FileItemC")},
481 {QStringLiteral("kfileitem#totalSize"), QStringLiteral("0FileItemC")},
482 {QStringLiteral("kfileitem#hiddenItems"), QStringLiteral("0FileItemD")},
483 {QStringLiteral("kfileitem#modified"), QStringLiteral("0FileItemE")},
484 {QStringLiteral("kfileitem#accessed"), QStringLiteral("0FileItemF")},
485 {QStringLiteral("kfileitem#created"), QStringLiteral("0FileItemG")},
486 {QStringLiteral("kfileitem#owner"), QStringLiteral("0FileItemH")},
487 {QStringLiteral("kfileitem#group"), QStringLiteral("0FileItemI")},
488 {QStringLiteral("kfileitem#permissions"), QStringLiteral("0FileItemJ")},
489
490 // Editable Data
491 {QStringLiteral("tags"), QStringLiteral("1EditableDataA")},
492 {QStringLiteral("rating"), QStringLiteral("1EditableDataB")},
493 {QStringLiteral("userComment"), QStringLiteral("1EditableDataC")},
494
495 // Image Data
496 {QStringLiteral("width"), QStringLiteral("2ImageA")},
497 {QStringLiteral("height"), QStringLiteral("2ImageB")},
498 {QStringLiteral("dimensions"), QStringLiteral("2ImageCA")},
499 {QStringLiteral("photoFNumber"), QStringLiteral("2ImageC")},
500 {QStringLiteral("photoExposureTime"), QStringLiteral("2ImageD")},
501 {QStringLiteral("photoExposureBiasValue"), QStringLiteral("2ImageE")},
502 {QStringLiteral("photoISOSpeedRatings"), QStringLiteral("2ImageF")},
503 {QStringLiteral("photoFocalLength"), QStringLiteral("2ImageG")},
504 {QStringLiteral("photoFocalLengthIn35mmFilm"), QStringLiteral("2ImageH")},
505 {QStringLiteral("photoFlash"), QStringLiteral("2ImageI")},
506 {QStringLiteral("imageOrientation"), QStringLiteral("2ImageJ")},
507 {QStringLiteral("photoGpsLocation"), QStringLiteral("2ImageK")},
508 {QStringLiteral("photoGpsLatitude"), QStringLiteral("2ImageL")},
509 {QStringLiteral("photoGpsLongitude"), QStringLiteral("2ImageM")},
510 {QStringLiteral("photoGpsAltitude"), QStringLiteral("2ImageN")},
511 {QStringLiteral("manufacturer"), QStringLiteral("2ImageO")},
512 {QStringLiteral("model"), QStringLiteral("2ImageP")},
513
514 // Media Data
515 {QStringLiteral("title"), QStringLiteral("3MediaA")},
516 {QStringLiteral("artist"), QStringLiteral("3MediaB")},
517 {QStringLiteral("album"), QStringLiteral("3MediaC")},
518 {QStringLiteral("albumArtist"), QStringLiteral("3MediaD")},
519 {QStringLiteral("genre"), QStringLiteral("3MediaE")},
520 {QStringLiteral("trackNumber"), QStringLiteral("3MediaF")},
521 {QStringLiteral("discNumber"), QStringLiteral("3MediaG")},
522 {QStringLiteral("releaseYear"), QStringLiteral("3MediaH")},
523 {QStringLiteral("duration"), QStringLiteral("3MediaI")},
524 {QStringLiteral("sampleRate"), QStringLiteral("3MediaJ")},
525 {QStringLiteral("bitRate"), QStringLiteral("3MediaK")},
526
527 // Miscellaneous Data
528 {QStringLiteral("originUrl"), QStringLiteral("4MiscA")},
529 };
530
531 const QString val = uriGrouper.value(label);
532 if (val.isEmpty()) {
533 return QStringLiteral("lastGroup");
534 }
535 return val;
536}
537
538KFileItemList FileMetaDataProvider::items() const
539{
540 return d->m_fileItems;
541}
542
544{
545 d->m_readOnly = readOnly;
546}
547
548bool FileMetaDataProvider::isReadOnly() const
549{
550 return d->m_readOnly;
551}
552
553QVariantMap FileMetaDataProvider::data() const
554{
555 return d->m_data;
556}
557
558#include "moc_filemetadataprovider.cpp"
Provides the data for the MetaDataWidget.
void loadingFinished()
Emitted once per KFileMetaDataProvider::setItems() after data loading is finished.
void setItems(const KFileItemList &items)
Sets the items, where the meta data should be requested.
virtual QString group(const QString &label) const
Meta data items are sorted alphabetically by their translated label per default.
void setReadOnly(bool readOnly)
If set to true, data such as the comment, tag or rating cannot be changed by the user.
virtual QString label(const QString &metaDataLabel) const
bool shouldBeIndexed(const QString &path) const
bool isSlow() const
QUrl targetUrl() const
bool isLocalFile() const
QString user() const
KIO::filesize_t size() const
bool isLink() const
Q_INVOKABLE QDateTime time(KFileItem::FileTimes which) const
KIO::filesize_t recursiveSize() const
QString group() const
QString linkDest() const
bool isDir() const
KIO::UDSEntry entry() const
QString permissionsString() const
QString mimeComment() const
QUrl url() const
QString displayName() const
static PropertyInfo fromName(const QString &name)
QString formatByteSize(double size, int precision=1, KFormat::BinaryUnitDialect dialect=KFormat::DefaultBinaryDialect, KFormat::BinarySizeUnits units=KFormat::DefaultBinaryUnits) const
QString stringValue(uint field) const
bool contains(uint field) const
void finished(KJob *job)
virtual Q_SCRIPTABLE void start()=0
static ExtraFieldList extraFields(const QUrl &url)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18ncp(const char *context, const char *singular, const char *plural, const TYPE &arg...)
KIOCORE_EXPORT QString dir(const QString &fileClass)
KCOREADDONS_EXPORT QString tildeCollapse(const QString &path)
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
bool isValid() const const
QByteArray encodeName(const QString &fileName)
T value(const Key &key) const const
qsizetype count() const const
pointer data()
T & first()
bool isEmpty() const const
void reserve(qsizetype size)
qsizetype size() const const
Q_EMITQ_EMIT
Q_SLOTSQ_SLOTS
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
qsizetype count() const const
QString arg(Args &&... args) const const
bool isEmpty() const const
QString number(double n, char format, int precision)
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
PreferLocalFile
bool isLocalFile() const const
QString path(ComponentFormattingOptions options) const const
QString scheme() const const
void setScheme(const QString &scheme)
QString toDisplayString(FormattingOptions options) const const
QString toLocalFile() const const
QVariant fromValue(T &&value)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 24 2024 12:00:13 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.