1 /*
2    SPDX-FileCopyrightText: 2016 (c) Matthieu Gallien <matthieu_gallien@yahoo.fr>
3 
4    SPDX-License-Identifier: LGPL-3.0-or-later
5  */
6 
7 #include "localbaloofilelisting.h"
8 
9 #include "baloo/baloocommon.h"
10 
11 #include "elisa_settings.h"
12 #include "elisautils.h"
13 
14 #include "baloo/scheduler.h"
15 #include "baloo/fileindexer.h"
16 #include "baloo/main.h"
17 
18 #include "baloowatcherapplicationadaptor.h"
19 
20 #include "filescanner.h"
21 
22 #include <Baloo/Query>
23 #include <Baloo/File>
24 #include <Baloo/IndexerConfig>
25 
26 #include <KFileMetaData/Properties>
27 #include <KFileMetaData/UserMetaData>
28 
29 
30 #include <QDBusConnection>
31 #include <QDBusConnectionInterface>
32 #include <QDBusServiceWatcher>
33 
34 #include <QThread>
35 #include <QHash>
36 #include <QFileInfo>
37 #include <QAtomicInt>
38 #include <QScopedPointer>
39 
40 #include <algorithm>
41 #include <memory>
42 
43 class LocalBalooFileListingPrivate
44 {
45 public:
46 
47     Baloo::Query mQuery;
48 
49     QDBusServiceWatcher mServiceWatcher;
50 
51     QScopedPointer<org::kde::baloo::main> mBalooMainInterface;
52 
53     QScopedPointer<org::kde::baloo::fileindexer> mBalooIndexer;
54 
55     QScopedPointer<org::kde::baloo::scheduler> mBalooScheduler;
56 
57     BalooWatcherApplicationAdaptor *mDbusAdaptor = nullptr;
58 
59     QAtomicInt mStopRequest = 0;
60 
61     bool mIsRegisteredToBaloo = false;
62 
63     bool mIsRegisteringToBaloo = false;
64 
65     bool mIsRegisteredToBalooWatcher = false;
66 
67     bool mIsRegisteringToBalooWatcher = false;
68 
69 };
70 
LocalBalooFileListing(QObject * parent)71 LocalBalooFileListing::LocalBalooFileListing(QObject *parent)
72     : AbstractFileListing(parent), d(std::make_unique<LocalBalooFileListingPrivate>())
73 {
74     d->mQuery.addType(QStringLiteral("Audio"));
75     setHandleNewFiles(false);
76 
77     auto sessionBus = QDBusConnection::sessionBus();
78 
79     d->mDbusAdaptor = new BalooWatcherApplicationAdaptor(this);
80 
81     sessionBus.registerObject(QStringLiteral("/org/kde/BalooWatcherApplication"), d->mDbusAdaptor, QDBusConnection::ExportAllContents);
82 
83     connect(&d->mServiceWatcher, &QDBusServiceWatcher::serviceRegistered,
84             this, &LocalBalooFileListing::serviceRegistered);
85     connect(&d->mServiceWatcher, &QDBusServiceWatcher::serviceOwnerChanged,
86             this, &LocalBalooFileListing::serviceOwnerChanged);
87     connect(&d->mServiceWatcher, &QDBusServiceWatcher::serviceUnregistered,
88             this, &LocalBalooFileListing::serviceUnregistered);
89 
90     d->mServiceWatcher.setConnection(sessionBus);
91     d->mServiceWatcher.addWatchedService(QStringLiteral("org.kde.baloo"));
92 
93     if (sessionBus.interface()->isServiceRegistered(QStringLiteral("org.kde.baloo"))) {
94         registerToBaloo();
95     }
96 }
97 
~LocalBalooFileListing()98 LocalBalooFileListing::~LocalBalooFileListing()
99 {
100     d->mDbusAdaptor->setParent(nullptr);
101     d->mDbusAdaptor->deleteLater();
102 }
103 
applicationAboutToQuit()104 void LocalBalooFileListing::applicationAboutToQuit()
105 {
106     AbstractFileListing::applicationAboutToQuit();
107     d->mStopRequest = 1;
108 }
109 
canHandleRootPaths() const110 bool LocalBalooFileListing::canHandleRootPaths() const
111 {
112     Baloo::IndexerConfig balooConfiguration;
113 
114     auto balooIncludedFolders = balooConfiguration.includeFolders();
115     auto balooExcludedFolders = balooConfiguration.excludeFolders();
116 
117     for (const auto &onePath : allRootPaths()) {
118         auto onePathInfo = QFileInfo{onePath};
119         auto onePathCanonicalPath = onePathInfo.canonicalFilePath();
120 
121         auto includedPath = false;
122 
123         for (const auto &balooIncludedPath : balooIncludedFolders) {
124             auto balooIncludedPathInfo = QFileInfo{balooIncludedPath};
125             auto balooIncludedCanonicalPath = balooIncludedPathInfo.canonicalFilePath();
126 
127             if (onePathCanonicalPath.startsWith(balooIncludedCanonicalPath)) {
128                 includedPath = true;
129                 break;
130             }
131         }
132 
133         for (const auto &balooExcludedPath : balooExcludedFolders) {
134             auto balooExcludedPathInfo = QFileInfo{balooExcludedPath};
135             auto balooExcludedCanonicalPath = balooExcludedPathInfo.canonicalFilePath();
136 
137             if (onePathCanonicalPath.startsWith(balooExcludedCanonicalPath)) {
138                 includedPath = false;
139                 break;
140             }
141         }
142 
143         if (!includedPath) {
144             return false;
145         }
146     }
147     return true;
148 }
149 
newBalooFile(const QString & fileName)150 void LocalBalooFileListing::newBalooFile(const QString &fileName)
151 {
152     qCDebug(orgKdeElisaBaloo()) << "LocalBalooFileListing::newBalooFile" << fileName;
153 
154     if (!isActive()) {
155         qCDebug(orgKdeElisaBaloo()) << "LocalBalooFileListing::newBalooFile is inactive";
156         return;
157     }
158 
159     auto scanFileInfo = QFileInfo(fileName);
160 
161     if (!scanFileInfo.exists()) {
162         return;
163     }
164 
165     if (!fileScanner().shouldScanFile(fileName)) {
166         return;
167     }
168 
169     Q_EMIT indexingStarted();
170 
171     auto newFile = QUrl::fromLocalFile(fileName);
172 
173     auto newTrack = scanOneFile(newFile, scanFileInfo, DoNotWatchFileSystemChanges);
174 
175     if (newTrack.isValid()) {
176         QFileInfo newFileInfo(fileName);
177 
178         addFileInDirectory(newFile, QUrl::fromLocalFile(newFileInfo.absoluteDir().absolutePath()), DoNotWatchFileSystemChanges);
179 
180         emitNewFiles({newTrack});
181     }
182 
183     Q_EMIT indexingFinished();
184 }
185 
registeredToBaloo(QDBusPendingCallWatcher * watcher)186 void LocalBalooFileListing::registeredToBaloo(QDBusPendingCallWatcher *watcher)
187 {
188     qCDebug(orgKdeElisaBaloo) << "LocalBalooFileListing::registeredToBaloo";
189 
190     if (!watcher) {
191         return;
192     }
193 
194     QDBusPendingReply<> reply = *watcher;
195     if (reply.isError()) {
196         qCDebug(orgKdeElisaBaloo) << "LocalBalooFileListing::registeredToBaloo" << reply.error().name() << reply.error().message();
197         d->mIsRegisteredToBaloo = false;
198     } else {
199         d->mIsRegisteredToBaloo = true;
200     }
201 
202     d->mIsRegisteringToBaloo = false;
203 
204     watcher->deleteLater();
205 }
206 
registeredToBalooWatcher(QDBusPendingCallWatcher * watcher)207 void LocalBalooFileListing::registeredToBalooWatcher(QDBusPendingCallWatcher *watcher)
208 {
209     qCDebug(orgKdeElisaBaloo) << "LocalBalooFileListing::registeredToBalooWatcher";
210 
211     if (!watcher) {
212         return;
213     }
214 
215     QDBusPendingReply<> reply = *watcher;
216     if (reply.isError()) {
217         qCDebug(orgKdeElisaBaloo) << "LocalBalooFileListing::registeredToBalooWatcher" << reply.error().name() << reply.error().message();
218         d->mIsRegisteredToBalooWatcher = false;
219     } else {
220         d->mIsRegisteredToBalooWatcher = true;
221     }
222 
223     d->mIsRegisteringToBalooWatcher = false;
224 
225     watcher->deleteLater();
226 }
227 
registerToBaloo()228 void LocalBalooFileListing::registerToBaloo()
229 {
230     if (d->mIsRegisteringToBaloo || d->mIsRegisteringToBalooWatcher) {
231         qCDebug(orgKdeElisaBaloo) << "LocalBalooFileListing::registerToBaloo" << "already registering";
232         return;
233     }
234 
235     qCDebug(orgKdeElisaBaloo) << "LocalBalooFileListing::registerToBaloo";
236 
237     d->mIsRegisteringToBaloo = true;
238     d->mIsRegisteringToBalooWatcher = true;
239 
240     auto sessionBus = QDBusConnection::sessionBus();
241 
242     d->mBalooMainInterface.reset(new org::kde::baloo::main(QStringLiteral("org.kde.baloo"), QStringLiteral("/"),
243                                                            sessionBus, this));
244 
245     if (!d->mBalooMainInterface->isValid()) {
246         qCDebug(orgKdeElisaBaloo) << "LocalBalooFileListing::registerToBaloo" << "invalid org.kde.baloo/main interface";
247         return;
248     }
249 
250     d->mBalooIndexer.reset(new org::kde::baloo::fileindexer(QStringLiteral("org.kde.baloo"), QStringLiteral("/fileindexer"),
251                                                             sessionBus, this));
252 
253     if (!d->mBalooIndexer->isValid()) {
254         qCDebug(orgKdeElisaBaloo) << "LocalBalooFileListing::registerToBaloo" << "invalid org.kde.baloo/fileindexer interface";
255         return;
256     }
257 
258     connect(d->mBalooIndexer.data(), &org::kde::baloo::fileindexer::finishedIndexingFile,
259             this, &LocalBalooFileListing::newBalooFile);
260 
261     d->mBalooScheduler.reset(new org::kde::baloo::scheduler(QStringLiteral("org.kde.baloo"), QStringLiteral("/scheduler"),
262                                                             sessionBus, this));
263 
264     if (!d->mBalooScheduler->isValid()) {
265         qCDebug(orgKdeElisaBaloo) << "LocalBalooFileListing::registerToBaloo" << "invalid org.kde.baloo/scheduler interface";
266         return;
267     }
268 
269     qCDebug(orgKdeElisaBaloo) << "LocalBalooFileListing::registerToBaloo" << "call registerMonitor";
270     auto answer = d->mBalooIndexer->registerMonitor();
271 
272     if (answer.isError()) {
273         qCDebug(orgKdeElisaBaloo) << "LocalBalooFileListing::executeInit" << answer.error().name() << answer.error().message();
274     }
275 
276     auto pendingCallWatcher = new QDBusPendingCallWatcher(answer);
277 
278     connect(pendingCallWatcher, &QDBusPendingCallWatcher::finished, this, &LocalBalooFileListing::registeredToBaloo);
279     if (pendingCallWatcher->isFinished()) {
280         registeredToBaloo(pendingCallWatcher);
281     }
282 
283     auto pendingCall = d->mBalooMainInterface->registerBalooWatcher(QStringLiteral("org.mpris.MediaPlayer2.elisa/org/kde/BalooWatcherApplication"));
284     qCDebug(orgKdeElisaBaloo) << "LocalBalooFileListing::registerToBaloo" << "call registerBalooWatcher";
285     auto pendingCallWatcher2 = new QDBusPendingCallWatcher(pendingCall);
286 
287     connect(pendingCallWatcher2, &QDBusPendingCallWatcher::finished, this, &LocalBalooFileListing::registeredToBalooWatcher);
288     if (pendingCallWatcher2->isFinished()) {
289         registeredToBalooWatcher(pendingCallWatcher2);
290     }
291 }
292 
renamedFiles(const QString & from,const QString & to,const QStringList & listFiles)293 void LocalBalooFileListing::renamedFiles(const QString &from, const QString &to, const QStringList &listFiles)
294 {
295     qCDebug(orgKdeElisaBaloo) << "LocalBalooFileListing::renamedFiles" << from << to << listFiles;
296 }
297 
serviceOwnerChanged(const QString & serviceName,const QString & oldOwner,const QString & newOwner)298 void LocalBalooFileListing::serviceOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner)
299 {
300     Q_UNUSED(oldOwner);
301     Q_UNUSED(newOwner);
302 
303     qCDebug(orgKdeElisaBaloo) << "LocalBalooFileListing::serviceOwnerChanged" << serviceName << oldOwner << newOwner;
304 
305     if (serviceName == QLatin1String("org.kde.baloo") && !newOwner.isEmpty()) {
306         d->mIsRegisteredToBaloo = false;
307         d->mIsRegisteredToBalooWatcher = false;
308         registerToBaloo();
309     }
310 }
311 
serviceRegistered(const QString & serviceName)312 void LocalBalooFileListing::serviceRegistered(const QString &serviceName)
313 {
314     qCDebug(orgKdeElisaBaloo) << "LocalBalooFileListing::serviceRegistered" << serviceName;
315 
316     if (serviceName == QLatin1String("org.kde.baloo")) {
317         registerToBaloo();
318     }
319 }
320 
serviceUnregistered(const QString & serviceName)321 void LocalBalooFileListing::serviceUnregistered(const QString &serviceName)
322 {
323     qCDebug(orgKdeElisaBaloo) << "LocalBalooFileListing::serviceUnregistered" << serviceName;
324 
325     if (serviceName == QLatin1String("org.kde.baloo")) {
326         d->mIsRegisteredToBaloo = false;
327         d->mIsRegisteredToBalooWatcher = false;
328     }
329 }
330 
executeInit(QHash<QUrl,QDateTime> allFiles)331 void LocalBalooFileListing::executeInit(QHash<QUrl, QDateTime> allFiles)
332 {
333     if (!isActive()) {
334         qCDebug(orgKdeElisaBaloo()) << "LocalBalooFileListing::executeInit is inactive";
335         return;
336     }
337 
338     qCDebug(orgKdeElisaBaloo()) << "LocalBalooFileListing::executeInit" << "with" << allFiles.size() << "files";
339     AbstractFileListing::executeInit(std::move(allFiles));
340 }
341 
triggerRefreshOfContent()342 void LocalBalooFileListing::triggerRefreshOfContent()
343 {
344     qCDebug(orgKdeElisaBaloo()) << "LocalBalooFileListing::triggerRefreshOfContent";
345 
346     if (!isActive()) {
347         qCDebug(orgKdeElisaBaloo()) << "LocalBalooFileListing::triggerRefreshOfContent is inactive";
348         return;
349     }
350 
351     Q_EMIT indexingStarted();
352 
353     AbstractFileListing::triggerRefreshOfContent();
354 
355     const auto &rootPaths = allRootPaths();
356     bool hasSingleRootPath = (rootPaths.size() == 1);
357     auto singleRootPath = rootPaths.at(0);
358 
359     auto resultIterator = d->mQuery.exec();
360     auto newFiles = DataTypes::ListTrackDataType();
361 
362     while(resultIterator.next() && d->mStopRequest == 0) {
363         const auto &fileName = resultIterator.filePath();
364 
365         if (hasSingleRootPath) {
366             if (!fileName.startsWith(singleRootPath)) {
367                 qCDebug(orgKdeElisaBaloo()) << "LocalBalooFileListing::triggerRefreshOfContent" << fileName << "does not match root paths";
368                 continue;
369             }
370         } else {
371             bool isIncluded = false;
372             for (const auto &oneRootPath : rootPaths) {
373                 if (fileName.startsWith(oneRootPath)) {
374                     isIncluded = true;
375                     break;
376                 }
377             }
378             if (!isIncluded) {
379                 qCDebug(orgKdeElisaBaloo()) << "LocalBalooFileListing::triggerRefreshOfContent" << fileName << "does not match root paths";
380                 continue;
381             }
382         }
383 
384         const auto &newFileUrl = QUrl::fromLocalFile(resultIterator.filePath());
385 
386         auto scanFileInfo = QFileInfo(fileName);
387 
388         if (!scanFileInfo.exists()) {
389             qCDebug(orgKdeElisaBaloo()) << "LocalBalooFileListing::triggerRefreshOfContent" << fileName << "file does not exists";
390             continue;
391         }
392 
393         auto itExistingFile = allFiles().find(newFileUrl);
394         if (itExistingFile != allFiles().end()) {
395             if (*itExistingFile >= scanFileInfo.metadataChangeTime()) {
396                 allFiles().erase(itExistingFile);
397                 qCDebug(orgKdeElisaBaloo()) << "LocalBalooFileListing::triggerRefreshOfContent" << fileName << "file not modified since last scan";
398                 continue;
399             }
400         }
401 
402         const auto currentDirectory = QUrl::fromLocalFile(scanFileInfo.absoluteDir().absolutePath());
403 
404         addFileInDirectory(newFileUrl, currentDirectory, DoNotWatchFileSystemChanges);
405 
406         const auto &newTrack = scanOneFile(newFileUrl, scanFileInfo, DoNotWatchFileSystemChanges);
407 
408         if (newTrack.isValid()) {
409             newFiles.push_back(newTrack);
410             if (newFiles.size() > 500 && d->mStopRequest == 0) {
411                 qCDebug(orgKdeElisaBaloo()) << "LocalBalooFileListing::triggerRefreshOfContent" << "insert new tracks in database" << newFiles.count();
412                 emitNewFiles(newFiles);
413                 newFiles.clear();
414             }
415         } else {
416             qCDebug(orgKdeElisaBaloo()) << "LocalBalooFileListing::triggerRefreshOfContent" << fileName << "invalid track" << newTrack;
417         }
418     }
419 
420     if (!newFiles.isEmpty() && d->mStopRequest == 0) {
421         qCDebug(orgKdeElisaBaloo()) << "LocalBalooFileListing::triggerRefreshOfContent" << "insert new tracks in database" << newFiles.count();
422         emitNewFiles(newFiles);
423     }
424 
425     setWaitEndTrackRemoval(false);
426 
427     checkFilesToRemove();
428 
429     if (!waitEndTrackRemoval()) {
430         Q_EMIT indexingFinished();
431     }
432 }
433 
triggerStop()434 void LocalBalooFileListing::triggerStop()
435 {
436     qCDebug(orgKdeElisaBaloo()) << "LocalBalooFileListing::triggerStop";
437     AbstractFileListing::triggerStop();
438 }
439 
scanOneFile(const QUrl & scanFile,const QFileInfo & scanFileInfo,FileSystemWatchingModes watchForFileSystemChanges)440 DataTypes::TrackDataType LocalBalooFileListing::scanOneFile(const QUrl &scanFile, const QFileInfo &scanFileInfo, FileSystemWatchingModes watchForFileSystemChanges)
441 {
442 
443     auto trackData = fileScanner().scanOneBalooFile(scanFile, scanFileInfo);
444 
445     if (!trackData.isValid()) {
446         qCDebug(orgKdeElisaBaloo) << "LocalBalooFileListing::scanOneFile" << scanFile << "falling back to plain file metadata analysis";
447         trackData = AbstractFileListing::scanOneFile(scanFile, scanFileInfo, watchForFileSystemChanges);
448     }
449 
450     if (trackData.isValid()) {
451         addCover(trackData);
452     } else {
453         qCDebug(orgKdeElisaBaloo) << "LocalBalooFileListing::scanOneFile" << scanFile << "invalid track";
454     }
455 
456     return trackData;
457 }
458 
459 #include "moc_localbaloofilelisting.cpp"
460