File indexing completed on 2024-05-05 04:50:32
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 "abstractfilelisting.h" 0008 0009 #include "config-upnp-qt.h" 0010 0011 #include "abstractfile/indexercommon.h" 0012 0013 #include "filescanner.h" 0014 #include "elisa_settings.h" 0015 0016 #include <QThread> 0017 #include <QFileInfo> 0018 #include <QFile> 0019 #include <QDir> 0020 #include <QFileSystemWatcher> 0021 #include <QSet> 0022 #include <QPair> 0023 #include <QAtomicInt> 0024 0025 0026 #include <algorithm> 0027 #include <utility> 0028 0029 class AbstractFileListingPrivate 0030 { 0031 public: 0032 0033 QStringList mAllRootPaths; 0034 0035 QFileSystemWatcher mFileSystemWatcher; 0036 0037 QHash<QString, QUrl> mAllAlbumCover; 0038 0039 QHash<QUrl, QSet<QPair<QUrl, bool>>> mDiscoveredFiles; 0040 0041 FileScanner mFileScanner; 0042 0043 QHash<QUrl, QDateTime> mAllFiles; 0044 0045 QAtomicInt mStopRequest = 0; 0046 0047 int mImportedTracksCount = 0; 0048 0049 int mNewFilesEmitInterval = 1; 0050 0051 bool mHandleNewFiles = true; 0052 0053 bool mWaitEndTrackRemoval = false; 0054 0055 bool mErrorWatchingFileSystemChanges = false; 0056 0057 bool mIsActive = false; 0058 0059 }; 0060 0061 AbstractFileListing::AbstractFileListing(QObject *parent) : QObject(parent), d(std::make_unique<AbstractFileListingPrivate>()) 0062 { 0063 connect(&d->mFileSystemWatcher, &QFileSystemWatcher::directoryChanged, 0064 this, &AbstractFileListing::directoryChanged); 0065 connect(&d->mFileSystemWatcher, &QFileSystemWatcher::fileChanged, 0066 this, &AbstractFileListing::fileChanged); 0067 } 0068 0069 AbstractFileListing::~AbstractFileListing() 0070 = default; 0071 0072 void AbstractFileListing::init() 0073 { 0074 qCDebug(orgKdeElisaIndexer()) << "AbstractFileListing::init"; 0075 d->mIsActive = true; 0076 0077 const bool autoScan = Elisa::ElisaConfiguration::self()->scanAtStartup(); 0078 if (autoScan) { 0079 Q_EMIT askRestoredTracks(); 0080 } 0081 } 0082 0083 void AbstractFileListing::stop() 0084 { 0085 d->mIsActive = false; 0086 0087 triggerStop(); 0088 } 0089 0090 void AbstractFileListing::newTrackFile(const DataTypes::TrackDataType &partialTrack) 0091 { 0092 auto scanFileInfo = QFileInfo(partialTrack.resourceURI().toLocalFile()); 0093 const auto &newTrack = scanOneFile(partialTrack.resourceURI(), scanFileInfo, WatchChangedDirectories | WatchChangedFiles); 0094 0095 if (newTrack.isValid() && newTrack != partialTrack) { 0096 Q_EMIT modifyTracksList({newTrack}, d->mAllAlbumCover); 0097 } 0098 } 0099 0100 void AbstractFileListing::restoredTracks(QHash<QUrl, QDateTime> allFiles) 0101 { 0102 executeInit(std::move(allFiles)); 0103 0104 refreshContent(); 0105 } 0106 0107 void AbstractFileListing::setAllRootPaths(const QStringList &allRootPaths) 0108 { 0109 d->mAllRootPaths = allRootPaths; 0110 } 0111 0112 void AbstractFileListing::databaseFinishedInsertingTracksList() 0113 { 0114 } 0115 0116 void AbstractFileListing::databaseFinishedRemovingTracksList() 0117 { 0118 if (waitEndTrackRemoval()) { 0119 Q_EMIT indexingFinished(); 0120 setWaitEndTrackRemoval(false); 0121 } 0122 } 0123 0124 void AbstractFileListing::applicationAboutToQuit() 0125 { 0126 d->mStopRequest = 1; 0127 } 0128 0129 const QStringList &AbstractFileListing::allRootPaths() const 0130 { 0131 return d->mAllRootPaths; 0132 } 0133 0134 bool AbstractFileListing::canHandleRootPaths() const 0135 { 0136 return true; 0137 } 0138 0139 void AbstractFileListing::scanDirectory(DataTypes::ListTrackDataType &newFiles, const QUrl &path, FileSystemWatchingModes watchForFileSystemChanges) 0140 { 0141 if (d->mStopRequest == 1) { 0142 return; 0143 } 0144 0145 QDir rootDirectory(path.toLocalFile()); 0146 rootDirectory.refresh(); 0147 0148 if (rootDirectory.exists()) { 0149 if (watchForFileSystemChanges & WatchChangedDirectories) { 0150 watchPath(path.toLocalFile()); 0151 } 0152 } 0153 0154 auto currentDirectoryListingFiles = d->mDiscoveredFiles[path]; 0155 0156 auto currentFilesList = QSet<QUrl>(); 0157 0158 rootDirectory.refresh(); 0159 const auto entryList = rootDirectory.entryInfoList(QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); 0160 for (const auto &oneEntry : entryList) { 0161 auto newFilePath = QUrl::fromLocalFile(oneEntry.canonicalFilePath()); 0162 0163 if (oneEntry.isDir() || oneEntry.isFile()) { 0164 currentFilesList.insert(newFilePath); 0165 } 0166 } 0167 0168 auto removedTracks = QVector<QPair<QUrl, bool>>(); 0169 for (const auto &removedFilePath : currentDirectoryListingFiles) { 0170 auto itFilePath = std::find(currentFilesList.begin(), currentFilesList.end(), removedFilePath.first); 0171 0172 if (itFilePath != currentFilesList.end()) { 0173 continue; 0174 } 0175 0176 removedTracks.push_back(removedFilePath); 0177 } 0178 0179 auto allRemovedTracks = QList<QUrl>(); 0180 for (const auto &oneRemovedTrack : removedTracks) { 0181 if (oneRemovedTrack.second) { 0182 allRemovedTracks.push_back(oneRemovedTrack.first); 0183 } else { 0184 removeFile(oneRemovedTrack.first, allRemovedTracks); 0185 } 0186 } 0187 for (const auto &oneRemovedTrack : removedTracks) { 0188 currentDirectoryListingFiles.remove(oneRemovedTrack); 0189 currentDirectoryListingFiles.remove(oneRemovedTrack); 0190 } 0191 0192 if (!allRemovedTracks.isEmpty()) { 0193 Q_EMIT removedTracksList(allRemovedTracks); 0194 } 0195 0196 if (!d->mHandleNewFiles) { 0197 return; 0198 } 0199 0200 for (const auto &newFilePath : currentFilesList) { 0201 QFileInfo oneEntry(newFilePath.toLocalFile()); 0202 0203 if (oneEntry.isDir()) { 0204 addFileInDirectory(newFilePath, path, WatchChangedDirectories | WatchChangedFiles); 0205 scanDirectory(newFiles, newFilePath, WatchChangedDirectories | WatchChangedFiles); 0206 0207 if (d->mStopRequest == 1) { 0208 break; 0209 } 0210 0211 continue; 0212 } 0213 if (!oneEntry.isFile()) { 0214 continue; 0215 } 0216 0217 auto itExistingFile = allFiles().constFind(newFilePath); 0218 if (itExistingFile != allFiles().cend()) { 0219 if (*itExistingFile >= oneEntry.metadataChangeTime()) { 0220 allFiles().erase(itExistingFile); 0221 qCDebug(orgKdeElisaIndexer()) << "AbstractFileListing::scanDirectory" << newFilePath << "file not modified since last scan"; 0222 continue; 0223 } 0224 } 0225 0226 auto newTrack = scanOneFile(newFilePath, oneEntry, WatchChangedDirectories | WatchChangedFiles); 0227 0228 if (newTrack.isValid() && d->mStopRequest == 0) { 0229 addCover(newTrack); 0230 0231 addFileInDirectory(newTrack.resourceURI(), path, WatchChangedDirectories | WatchChangedFiles); 0232 newFiles.push_back(newTrack); 0233 0234 ++d->mImportedTracksCount; 0235 0236 if (newFiles.size() > d->mNewFilesEmitInterval && d->mStopRequest == 0) { 0237 d->mNewFilesEmitInterval = std::min(50, 1 + d->mNewFilesEmitInterval * d->mNewFilesEmitInterval); 0238 emitNewFiles(newFiles); 0239 newFiles.clear(); 0240 } 0241 } else { 0242 qCDebug(orgKdeElisaIndexer()) << "AbstractFileListing::scanDirectory" << newFilePath << "is not a valid track"; 0243 } 0244 0245 if (d->mStopRequest == 1) { 0246 break; 0247 } 0248 } 0249 } 0250 0251 void AbstractFileListing::directoryChanged(const QString &path) 0252 { 0253 const auto directoryEntry = d->mDiscoveredFiles.find(QUrl::fromLocalFile(path)); 0254 if (directoryEntry == d->mDiscoveredFiles.end()) { 0255 return; 0256 } 0257 0258 Q_EMIT indexingStarted(); 0259 0260 scanDirectoryTree(path); 0261 0262 Q_EMIT indexingFinished(); 0263 } 0264 0265 void AbstractFileListing::fileChanged(const QString &modifiedFileName) 0266 { 0267 QFileInfo modifiedFileInfo(modifiedFileName); 0268 auto modifiedFile = QUrl::fromLocalFile(modifiedFileName); 0269 0270 auto modifiedTrack = scanOneFile(modifiedFile, modifiedFileInfo, WatchChangedDirectories | WatchChangedFiles); 0271 0272 if (modifiedTrack.isValid()) { 0273 Q_EMIT modifyTracksList({modifiedTrack}, d->mAllAlbumCover); 0274 } 0275 } 0276 0277 void AbstractFileListing::executeInit(QHash<QUrl, QDateTime> allFiles) 0278 { 0279 d->mAllFiles = std::move(allFiles); 0280 } 0281 0282 void AbstractFileListing::triggerStop() 0283 { 0284 } 0285 0286 void AbstractFileListing::triggerRefreshOfContent() 0287 { 0288 d->mImportedTracksCount = 0; 0289 } 0290 0291 void AbstractFileListing::refreshContent() 0292 { 0293 triggerRefreshOfContent(); 0294 } 0295 0296 DataTypes::TrackDataType AbstractFileListing::scanOneFile(const QUrl &scanFile, const QFileInfo &scanFileInfo, FileSystemWatchingModes watchForFileSystemChanges) 0297 { 0298 DataTypes::TrackDataType newTrack; 0299 0300 qCDebug(orgKdeElisaIndexer) << "AbstractFileListing::scanOneFile" << scanFile; 0301 0302 auto localFileName = scanFile.toLocalFile(); 0303 0304 if (!d->mFileScanner.shouldScanFile(localFileName)) { 0305 qCDebug(orgKdeElisaIndexer) << "AbstractFileListing::scanOneFile" << "invalid mime type"; 0306 return newTrack; 0307 } 0308 0309 if (scanFileInfo.exists()) { 0310 auto itExistingFile = d->mAllFiles.constFind(scanFile); 0311 if (itExistingFile != d->mAllFiles.cend()) { 0312 if (*itExistingFile >= scanFileInfo.metadataChangeTime()) { 0313 d->mAllFiles.erase(itExistingFile); 0314 qCDebug(orgKdeElisaIndexer) << "AbstractFileListing::scanOneFile" << "not changed file"; 0315 return newTrack; 0316 } 0317 } 0318 } 0319 0320 newTrack = d->mFileScanner.scanOneFile(scanFile, scanFileInfo); 0321 0322 if (newTrack.isValid() && scanFileInfo.exists()) { 0323 if (watchForFileSystemChanges & WatchChangedFiles) { 0324 watchPath(scanFile.toLocalFile()); 0325 } 0326 } 0327 0328 return newTrack; 0329 } 0330 0331 void AbstractFileListing::watchPath(const QString &pathName) 0332 { 0333 if (!d->mFileSystemWatcher.addPath(pathName)) { 0334 qCDebug(orgKdeElisaIndexer) << "AbstractFileListing::watchPath" << "fail for" << pathName; 0335 0336 if (!d->mErrorWatchingFileSystemChanges) { 0337 d->mErrorWatchingFileSystemChanges = true; 0338 Q_EMIT errorWatchingFileSystemChanges(); 0339 } 0340 } 0341 } 0342 0343 void AbstractFileListing::addFileInDirectory(const QUrl &newFile, const QUrl &directoryName, FileSystemWatchingModes watchForFileSystemChanges) 0344 { 0345 const auto directoryEntry = d->mDiscoveredFiles.find(directoryName); 0346 if (directoryEntry == d->mDiscoveredFiles.end()) { 0347 if (watchForFileSystemChanges & WatchChangedDirectories) { 0348 watchPath(directoryName.toLocalFile()); 0349 } 0350 0351 QDir currentDirectory(directoryName.toLocalFile()); 0352 if (currentDirectory.cdUp()) { 0353 const auto parentDirectoryName = currentDirectory.absolutePath(); 0354 const auto parentDirectory = QUrl::fromLocalFile(parentDirectoryName); 0355 const auto parentDirectoryEntry = d->mDiscoveredFiles.find(parentDirectory); 0356 if (parentDirectoryEntry == d->mDiscoveredFiles.end()) { 0357 if (watchForFileSystemChanges & WatchChangedDirectories) { 0358 watchPath(parentDirectoryName); 0359 } 0360 } 0361 0362 auto &parentCurrentDirectoryListingFiles = d->mDiscoveredFiles[parentDirectory]; 0363 0364 parentCurrentDirectoryListingFiles.insert({directoryName, false}); 0365 } 0366 } 0367 auto ¤tDirectoryListingFiles = d->mDiscoveredFiles[directoryName]; 0368 0369 QFileInfo isAFile(newFile.toLocalFile()); 0370 currentDirectoryListingFiles.insert({newFile, isAFile.isFile()}); 0371 } 0372 0373 void AbstractFileListing::scanDirectoryTree(const QString &path) 0374 { 0375 auto newFiles = DataTypes::ListTrackDataType(); 0376 0377 qCDebug(orgKdeElisaIndexer()) << "AbstractFileListing::scanDirectoryTree" << path; 0378 0379 scanDirectory(newFiles, QUrl::fromLocalFile(path), WatchChangedDirectories | WatchChangedFiles); 0380 0381 if (!newFiles.isEmpty() && d->mStopRequest == 0) { 0382 emitNewFiles(newFiles); 0383 } 0384 } 0385 0386 void AbstractFileListing::setHandleNewFiles(bool handleThem) 0387 { 0388 d->mHandleNewFiles = handleThem; 0389 } 0390 0391 void AbstractFileListing::emitNewFiles(const DataTypes::ListTrackDataType &tracks) 0392 { 0393 Q_EMIT tracksList(tracks, d->mAllAlbumCover); 0394 } 0395 0396 void AbstractFileListing::addCover(const DataTypes::TrackDataType &newTrack) 0397 { 0398 auto itCover = d->mAllAlbumCover.find(newTrack.album()); 0399 if (itCover != d->mAllAlbumCover.end()) { 0400 return; 0401 } 0402 0403 auto coverUrl = d->mFileScanner.searchForCoverFile(newTrack.resourceURI().toLocalFile()); 0404 if (!coverUrl.isEmpty()) { 0405 d->mAllAlbumCover[newTrack.resourceURI().toString()] = coverUrl; 0406 } 0407 } 0408 0409 void AbstractFileListing::removeDirectory(const QUrl &removedDirectory, QList<QUrl> &allRemovedFiles) 0410 { 0411 const auto itRemovedDirectory = d->mDiscoveredFiles.constFind(removedDirectory); 0412 0413 if (itRemovedDirectory == d->mDiscoveredFiles.cend()) { 0414 return; 0415 } 0416 0417 const auto ¤tRemovedDirectory = *itRemovedDirectory; 0418 for (const auto &itFile : currentRemovedDirectory) { 0419 if (itFile.first.isValid() && !itFile.first.isEmpty()) { 0420 removeFile(itFile.first, allRemovedFiles); 0421 if (itFile.second) { 0422 allRemovedFiles.push_back(itFile.first); 0423 } 0424 } 0425 } 0426 0427 d->mDiscoveredFiles.erase(itRemovedDirectory); 0428 } 0429 0430 void AbstractFileListing::removeFile(const QUrl &oneRemovedTrack, QList<QUrl> &allRemovedFiles) 0431 { 0432 auto itRemovedDirectory = d->mDiscoveredFiles.find(oneRemovedTrack); 0433 if (itRemovedDirectory != d->mDiscoveredFiles.end()) { 0434 removeDirectory(oneRemovedTrack, allRemovedFiles); 0435 } 0436 } 0437 0438 QHash<QUrl, QDateTime> &AbstractFileListing::allFiles() 0439 { 0440 return d->mAllFiles; 0441 } 0442 0443 void AbstractFileListing::checkFilesToRemove() 0444 { 0445 QList<QUrl> allRemovedFiles; 0446 0447 for (auto itFile = d->mAllFiles.begin(); itFile != d->mAllFiles.end(); ++itFile) { 0448 allRemovedFiles.push_back(itFile.key()); 0449 } 0450 0451 qCDebug(orgKdeElisaIndexer()) << "AbstractFileListing::checkFilesToRemove" << allRemovedFiles.size(); 0452 0453 if (!allRemovedFiles.isEmpty()) { 0454 setWaitEndTrackRemoval(true); 0455 Q_EMIT removedTracksList(allRemovedFiles); 0456 } 0457 } 0458 0459 FileScanner &AbstractFileListing::fileScanner() 0460 { 0461 return d->mFileScanner; 0462 } 0463 0464 bool AbstractFileListing::waitEndTrackRemoval() const 0465 { 0466 return d->mWaitEndTrackRemoval; 0467 } 0468 0469 void AbstractFileListing::setWaitEndTrackRemoval(bool wait) 0470 { 0471 d->mWaitEndTrackRemoval = wait; 0472 } 0473 0474 bool AbstractFileListing::isActive() const 0475 { 0476 return d->mIsActive; 0477 } 0478 0479 0480 #include "moc_abstractfilelisting.cpp"