File indexing completed on 2024-04-21 04:49:01

0001 /*
0002    SPDX-FileCopyrightText: 2016 (c) Matthieu Gallien <matthieu_gallien@yahoo.fr>
0003 
0004    SPDX-License-Identifier: LGPL-3.0-or-later
0005  */
0006 
0007 #include "musiclistenersmanager.h"
0008 
0009 #include "config-upnp-qt.h"
0010 
0011 #include "indexersManager.h"
0012 
0013 #if UPNPQT_FOUND
0014 #include "upnp/upnplistener.h"
0015 #endif
0016 
0017 #if QtAndroidExtras_FOUND
0018 #include "android/androidmusiclistener.h"
0019 #endif
0020 
0021 #include "databaseinterface.h"
0022 #include "mediaplaylist.h"
0023 #include "file/filelistener.h"
0024 #include "file/localfilelisting.h"
0025 #include "trackslistener.h"
0026 #include "elisaapplication.h"
0027 #include "elisa_settings.h"
0028 #include "modeldataloader.h"
0029 
0030 #include <KLocalizedString>
0031 
0032 #include <QThread>
0033 #include <QStandardPaths>
0034 #include <QDir>
0035 #include <QCoreApplication>
0036 #include <QFileSystemWatcher>
0037 
0038 #include <list>
0039 
0040 class MusicListenersManagerPrivate
0041 {
0042 public:
0043 
0044     QThread mDatabaseThread;
0045 
0046     QThread mListenerThread;
0047 
0048 #if UPNPQT_FOUND
0049     UpnpListener mUpnpListener;
0050 #endif
0051 
0052     FileListener mFileListener;
0053 
0054 #if QtAndroidExtras_FOUND
0055     AndroidMusicListener mAndroidMusicListener;
0056 #endif
0057 
0058     DatabaseInterface mDatabaseInterface;
0059 
0060     std::unique_ptr<TracksListener> mTracksListener;
0061 
0062     QFileSystemWatcher mConfigFileWatcher;
0063 
0064     QStringList mPreviousRootPathValue;
0065 
0066     ElisaApplication *mElisaApplication = nullptr;
0067 
0068     int mImportedTracksCount = 0;
0069 
0070     bool mIndexerBusy = false;
0071 
0072     bool mFileSystemIndexerActive = false;
0073 
0074     bool mAndroidIndexerActive = false;
0075 
0076     bool mAndroidIndexerAvailable = false;
0077 
0078 };
0079 
0080 MusicListenersManager::MusicListenersManager(QObject *parent)
0081     : QObject(parent), d(std::make_unique<MusicListenersManagerPrivate>())
0082 {
0083     connect(&d->mDatabaseInterface, &DatabaseInterface::tracksAdded,
0084             this, &MusicListenersManager::increaseImportedTracksCount);
0085 
0086     connect(&d->mDatabaseInterface, &DatabaseInterface::requestsInitDone,
0087             this, &MusicListenersManager::databaseReady);
0088 
0089     connect(this, &MusicListenersManager::clearDatabase,
0090             &d->mDatabaseInterface, &DatabaseInterface::clearData);
0091 
0092     connect(&d->mDatabaseInterface, &DatabaseInterface::cleanedDatabase,
0093             this, &MusicListenersManager::cleanedDatabase);
0094 
0095     connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit,
0096             this, &MusicListenersManager::applicationAboutToQuit);
0097 
0098     connect(&d->mConfigFileWatcher, &QFileSystemWatcher::fileChanged,
0099             this, &MusicListenersManager::configChanged);
0100 
0101     connect(this, &MusicListenersManager::refreshDatabase,
0102             &d->mDatabaseInterface, &DatabaseInterface::askRestoredTracks);
0103 
0104     d->mListenerThread.start();
0105     d->mDatabaseThread.start();
0106 
0107     d->mDatabaseInterface.moveToThread(&d->mDatabaseThread);
0108 
0109     const auto &localDataPaths = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
0110     auto databaseFileName = QString();
0111     if (!localDataPaths.isEmpty()) {
0112         QDir myDataDirectory;
0113         myDataDirectory.mkpath(localDataPaths.first());
0114         databaseFileName = localDataPaths.first() + QStringLiteral("/elisaDatabase.db");
0115     }
0116 
0117     QMetaObject::invokeMethod(&d->mDatabaseInterface, "init", Qt::QueuedConnection,
0118                               Q_ARG(QString, QStringLiteral("listeners")), Q_ARG(QString, databaseFileName));
0119 
0120     qCInfo(orgKdeElisaIndexersManager) << "Local file system indexer is inactive";
0121 }
0122 
0123 MusicListenersManager::~MusicListenersManager()
0124 {
0125     d->mListenerThread.quit();
0126     d->mListenerThread.wait();
0127 
0128     d->mDatabaseThread.quit();
0129     d->mDatabaseThread.wait();
0130 }
0131 
0132 DatabaseInterface *MusicListenersManager::viewDatabase() const
0133 {
0134     return &d->mDatabaseInterface;
0135 }
0136 
0137 void MusicListenersManager::subscribeForTracks(MediaPlayList *client)
0138 {
0139     createTracksListener();
0140     connect(d->mTracksListener.get(), &TracksListener::trackHasChanged, client, &MediaPlayList::trackChanged);
0141     connect(d->mTracksListener.get(), &TracksListener::trackHasBeenRemoved, client, &MediaPlayList::trackRemoved);
0142     connect(d->mTracksListener.get(), &TracksListener::tracksListAdded, client, &MediaPlayList::tracksListAdded);
0143     connect(client, &MediaPlayList::newEntryInList, d->mTracksListener.get(), &TracksListener::newEntryInList);
0144     connect(client, &MediaPlayList::newUrlInList, d->mTracksListener.get(), &TracksListener::newUrlInList);
0145     connect(client, &MediaPlayList::newTrackByNameInList, d->mTracksListener.get(), &TracksListener::trackByNameInList);
0146 }
0147 
0148 int MusicListenersManager::importedTracksCount() const
0149 {
0150     return d->mImportedTracksCount;
0151 }
0152 
0153 ElisaApplication *MusicListenersManager::elisaApplication() const
0154 {
0155     return d->mElisaApplication;
0156 }
0157 
0158 TracksListener *MusicListenersManager::tracksListener() const
0159 {
0160     return d->mTracksListener.get();
0161 }
0162 
0163 bool MusicListenersManager::indexerBusy() const
0164 {
0165     return d->mIndexerBusy;
0166 }
0167 
0168 bool MusicListenersManager::fileSystemIndexerActive() const
0169 {
0170     return d->mFileSystemIndexerActive;
0171 }
0172 
0173 bool MusicListenersManager::androidIndexerActive() const
0174 {
0175     return d->mAndroidIndexerActive;
0176 }
0177 
0178 bool MusicListenersManager::androidIndexerAvailable() const
0179 {
0180     return d->mAndroidIndexerAvailable;
0181 }
0182 
0183 auto MusicListenersManager::initializeRootPath()
0184 {
0185     auto initialRootPath = QStringList{};
0186     auto systemMusicPaths = QStandardPaths::standardLocations(QStandardPaths::MusicLocation);
0187     for (const auto &musicPath : std::as_const(systemMusicPaths)) {
0188         initialRootPath.push_back(musicPath);
0189     }
0190 
0191     Elisa::ElisaConfiguration::setRootPath(initialRootPath);
0192     Elisa::ElisaConfiguration::self()->save();
0193 
0194     return initialRootPath;
0195 }
0196 
0197 void MusicListenersManager::databaseReady()
0198 {
0199     auto initialRootPath = Elisa::ElisaConfiguration::rootPath();
0200     if (initialRootPath.isEmpty()) {
0201         initializeRootPath();
0202     }
0203 
0204     d->mConfigFileWatcher.addPath(Elisa::ElisaConfiguration::self()->config()->name());
0205 
0206     configChanged();
0207 }
0208 
0209 void MusicListenersManager::applicationAboutToQuit()
0210 {
0211     d->mDatabaseInterface.applicationAboutToQuit();
0212 
0213     Q_EMIT applicationIsTerminating();
0214 
0215     d->mDatabaseThread.exit();
0216     d->mDatabaseThread.wait();
0217 
0218     d->mListenerThread.exit();
0219     d->mListenerThread.wait();
0220 }
0221 
0222 void MusicListenersManager::setElisaApplication(ElisaApplication *elisaApplication)
0223 {
0224     if (d->mElisaApplication == elisaApplication) {
0225         return;
0226     }
0227 
0228     d->mElisaApplication = elisaApplication;
0229     Q_EMIT elisaApplicationChanged();
0230 }
0231 
0232 void MusicListenersManager::playBackError(const QUrl &sourceInError, QMediaPlayer::Error playerError)
0233 {
0234     qCDebug(orgKdeElisaIndexersManager) << "MusicListenersManager::playBackError" << sourceInError;
0235 
0236     if (playerError == QMediaPlayer::ResourceError) {
0237         Q_EMIT removeTracksInError({sourceInError});
0238 
0239         if (sourceInError.isLocalFile()) {
0240             Q_EMIT displayTrackError(sourceInError.toLocalFile());
0241         } else {
0242             Q_EMIT displayTrackError(sourceInError.toString());
0243         }
0244     }
0245 }
0246 
0247 void MusicListenersManager::deleteElementById(ElisaUtils::PlayListEntryType entryType, qulonglong databaseId)
0248 {
0249     switch(entryType)
0250     {
0251     case ElisaUtils::Radio:
0252         QMetaObject::invokeMethod(&d->mDatabaseInterface, "removeRadio", Qt::QueuedConnection,
0253                                   Q_ARG(qulonglong, databaseId));
0254         break;
0255     case ElisaUtils::Album:
0256     case ElisaUtils::Artist:
0257     case ElisaUtils::Genre:
0258     case ElisaUtils::Lyricist:
0259     case ElisaUtils::Composer:
0260     case ElisaUtils::Track:
0261     case ElisaUtils::FileName:
0262     case ElisaUtils::Unknown:
0263     case ElisaUtils::Container:
0264     case ElisaUtils::PlayList:
0265         break;
0266     }
0267 }
0268 
0269 void MusicListenersManager::connectModel(ModelDataLoader *dataLoader)
0270 {
0271     dataLoader->moveToThread(&d->mDatabaseThread);
0272 }
0273 
0274 void MusicListenersManager::scanCollection(CollectionScan scantype)
0275 {
0276     switch (scantype)
0277     {
0278     case CollectionScan::Hard:
0279         Q_EMIT clearDatabase();
0280         break;
0281     case CollectionScan::Soft:
0282         Q_EMIT refreshDatabase();
0283     }
0284 }
0285 
0286 void MusicListenersManager::configChanged()
0287 {
0288     auto currentConfiguration = Elisa::ElisaConfiguration::self();
0289 
0290     d->mConfigFileWatcher.addPath(currentConfiguration->config()->name());
0291 
0292     currentConfiguration->load();
0293     currentConfiguration->read();
0294 
0295     bool configurationHasChanged = false;
0296 
0297     auto inputRootPath = currentConfiguration->rootPath();
0298     configurationHasChanged = configurationHasChanged || (d->mPreviousRootPathValue != inputRootPath);
0299 
0300     if (configurationHasChanged) {
0301         d->mPreviousRootPathValue = inputRootPath;
0302     } else {
0303         qCDebug(orgKdeElisaIndexersManager()) << "music paths configuration and scanning has not changed";
0304         return;
0305     }
0306 
0307     //resolve symlinks
0308     QStringList allRootPaths;
0309     for (const auto &onePath : std::as_const(inputRootPath)) {
0310         auto workPath = onePath;
0311         if (workPath.startsWith(QLatin1String("file:/"))) {
0312             auto urlPath = QUrl{workPath};
0313             workPath = urlPath.toLocalFile();
0314         }
0315 
0316         QFileInfo pathFileInfo(workPath);
0317         auto directoryPath = pathFileInfo.canonicalFilePath();
0318         if (!directoryPath.isEmpty()) {
0319             if (QStringView(directoryPath).right(1) != QLatin1Char('/'))
0320             {
0321                 directoryPath.append(QLatin1Char('/'));
0322             }
0323             allRootPaths.push_back(directoryPath);
0324         }
0325     }
0326 
0327     if (allRootPaths.isEmpty()) {
0328         allRootPaths = initializeRootPath();
0329     }
0330 
0331     d->mFileListener.setAllRootPaths(allRootPaths);
0332 
0333     // trigger start of indexers if needed
0334     startLocalFileSystemIndexing();
0335     startAndroidIndexing();
0336 
0337     if (d->mFileSystemIndexerActive) {
0338         qCInfo(orgKdeElisaIndexersManager()) << "trigger init of local file indexer";
0339         QMetaObject::invokeMethod(d->mFileListener.fileListing(), "init", Qt::QueuedConnection);
0340     } else if (d->mAndroidIndexerActive) {
0341 #if defined Q_OS_ANDROID
0342         QMetaObject::invokeMethod(d->mAndroidMusicListener.fileListing(), "init", Qt::QueuedConnection);
0343 #endif
0344     }
0345 
0346 #if UPNPQT_FOUND
0347     d->mUpnpListener.setDatabaseInterface(&d->mDatabaseInterface);
0348     d->mUpnpListener.moveToThread(&d->mDatabaseThread);
0349     connect(this, &MusicListenersManager::applicationIsTerminating,
0350             &d->mUpnpListener, &UpnpListener::applicationAboutToQuit, Qt::DirectConnection);
0351 #endif
0352 
0353 }
0354 
0355 void MusicListenersManager::increaseImportedTracksCount(const DataTypes::ListTrackDataType &allTracks)
0356 {
0357     d->mImportedTracksCount += allTracks.size();
0358 
0359     Q_EMIT importedTracksCountChanged();
0360 }
0361 
0362 void MusicListenersManager::decreaseImportedTracksCount()
0363 {
0364     --d->mImportedTracksCount;
0365 
0366     Q_EMIT importedTracksCountChanged();
0367 }
0368 
0369 void MusicListenersManager::monitorStartingListeners()
0370 {
0371     d->mIndexerBusy = true;
0372     Q_EMIT indexerBusyChanged();
0373 }
0374 
0375 void MusicListenersManager::monitorEndingListeners()
0376 {
0377     d->mIndexerBusy = false;
0378     Q_EMIT indexerBusyChanged();
0379 }
0380 
0381 void MusicListenersManager::cleanedDatabase()
0382 {
0383     d->mImportedTracksCount = 0;
0384     Q_EMIT importedTracksCountChanged();
0385     Q_EMIT clearedDatabase();
0386 }
0387 
0388 void MusicListenersManager::startLocalFileSystemIndexing()
0389 {
0390     if (d->mFileSystemIndexerActive) {
0391         return;
0392     }
0393 
0394     d->mFileListener.setDatabaseInterface(&d->mDatabaseInterface);
0395     d->mFileListener.moveToThread(&d->mListenerThread);
0396     connect(this, &MusicListenersManager::applicationIsTerminating,
0397             &d->mFileListener, &FileListener::applicationAboutToQuit, Qt::DirectConnection);
0398     connect(&d->mFileListener, &FileListener::indexingStarted,
0399             this, &MusicListenersManager::monitorStartingListeners);
0400     connect(&d->mFileListener, &FileListener::indexingFinished,
0401             this, &MusicListenersManager::monitorEndingListeners);
0402 
0403     qCInfo(orgKdeElisaIndexersManager) << "Local file system indexer is active";
0404 
0405     d->mFileSystemIndexerActive = true;
0406     Q_EMIT fileSystemIndexerActiveChanged();
0407 }
0408 
0409 void MusicListenersManager::startAndroidIndexing()
0410 {
0411     qCInfo(orgKdeElisaIndexersManager) << "MusicListenersManager::startAndroidIndexing";
0412 #if defined Q_OS_ANDROID
0413     if (d->mAndroidIndexerActive) {
0414         return;
0415     }
0416 
0417     d->mAndroidMusicListener.setDatabaseInterface(&d->mDatabaseInterface);
0418     d->mAndroidMusicListener.moveToThread(&d->mListenerThread);
0419     connect(this, &MusicListenersManager::applicationIsTerminating,
0420             &d->mAndroidMusicListener, &AndroidMusicListener::applicationAboutToQuit, Qt::DirectConnection);
0421     connect(&d->mAndroidMusicListener, &AndroidMusicListener::indexingStarted,
0422             this, &MusicListenersManager::monitorStartingListeners);
0423     connect(&d->mAndroidMusicListener, &AndroidMusicListener::indexingFinished,
0424             this, &MusicListenersManager::monitorEndingListeners);
0425 
0426     qCInfo(orgKdeElisaIndexersManager) << "Android indexer is active";
0427 
0428     d->mAndroidIndexerActive = true;
0429     Q_EMIT androidIndexerActiveChanged();
0430 #endif
0431 }
0432 
0433 void MusicListenersManager::createTracksListener()
0434 {
0435     if (!d->mTracksListener) {
0436         d->mTracksListener = std::make_unique<TracksListener>(&d->mDatabaseInterface);
0437         d->mTracksListener->moveToThread(&d->mDatabaseThread);
0438 
0439         connect(this, &MusicListenersManager::removeTracksInError,
0440                 &d->mDatabaseInterface, &DatabaseInterface::removeTracksList);
0441 
0442         connect(&d->mDatabaseInterface, &DatabaseInterface::trackRemoved, d->mTracksListener.get(), &TracksListener::trackRemoved);
0443         connect(&d->mDatabaseInterface, &DatabaseInterface::tracksAdded, d->mTracksListener.get(), &TracksListener::tracksAdded);
0444         connect(&d->mDatabaseInterface, &DatabaseInterface::trackModified, d->mTracksListener.get(), &TracksListener::trackModified);
0445         Q_EMIT tracksListenerChanged();
0446     }
0447 }
0448 
0449 void MusicListenersManager::updateSingleFileMetaData(const QUrl &url, DataTypes::ColumnsRoles role, const QVariant &data)
0450 {
0451     tracksListener()->updateSingleFileMetaData(url, role, data);
0452 }
0453 
0454 #include "moc_musiclistenersmanager.cpp"