File indexing completed on 2025-01-19 04:24:26

0001 /****************************************************************************************
0002  * Copyright (c) 2007 Maximilian Kossick <maximilian.kossick@googlemail.com>            *
0003  * Copyright (c) 2008 Seb Ruiz <ruiz@kde.org>                                           *
0004  * Copyright (c) 2009-2010 Jeff Mitchell <mitchell@kde.org>                             *
0005  * Copyright (c) 2013 Ralf Engels <ralf-engels@gmx.de>                                  *
0006  *                                                                                      *
0007  * This program is free software; you can redistribute it and/or modify it under        *
0008  * the terms of the GNU General Public License as published by the Free Software        *
0009  * Foundation; either version 2 of the License, or (at your option) any later           *
0010  * version.                                                                             *
0011  *                                                                                      *
0012  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0013  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0014  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0015  *                                                                                      *
0016  * You should have received a copy of the GNU General Public License along with         *
0017  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0018  ****************************************************************************************/
0019 
0020 #define DEBUG_PREFIX "SqlScanResultProcessor"
0021 
0022 #include "SqlScanResultProcessor.h"
0023 
0024 #include "MainWindow.h"
0025 #include "collectionscanner/Directory.h"
0026 #include "collectionscanner/Album.h"
0027 #include "collectionscanner/Track.h"
0028 #include "collectionscanner/Playlist.h"
0029 #include "core/support/Debug.h"
0030 #include "core-impl/collections/db/MountPointManager.h"
0031 #include "core-impl/collections/db/sql/SqlQueryMaker.h"
0032 #include "playlistmanager/PlaylistManager.h"
0033 
0034 #include <KMessageBox>
0035 
0036 #include <QAbstractEventDispatcher>
0037 #include <QApplication>
0038 
0039 SqlScanResultProcessor::SqlScanResultProcessor( GenericScanManager* manager,
0040                                                 Collections::SqlCollection* collection,
0041                                                 QObject *parent )
0042     : AbstractScanResultProcessor( manager, parent )
0043     , m_collection( collection )
0044 { }
0045 
0046 SqlScanResultProcessor::~SqlScanResultProcessor()
0047 { }
0048 
0049 void
0050 SqlScanResultProcessor::scanStarted( GenericScanManager::ScanType type )
0051 {
0052     AbstractScanResultProcessor::scanStarted( type );
0053 
0054     m_collection->sqlStorage()->clearLastErrors();
0055     m_messages.clear();
0056 }
0057 
0058 void
0059 SqlScanResultProcessor::scanSucceeded()
0060 {
0061     DEBUG_BLOCK;
0062 
0063     // we are blocking the updated signal for maximum of one second.
0064     m_blockedTime = QDateTime::currentDateTime();
0065     blockUpdates();
0066 
0067     urlsCacheInit();
0068 
0069     // -- call the base implementation
0070     AbstractScanResultProcessor::scanSucceeded();
0071 
0072     // -- error reporting
0073     m_messages.append( m_collection->sqlStorage()->getLastErrors() );
0074 
0075     if( !m_messages.isEmpty() && qobject_cast<QGuiApplication*>(qApp) )
0076         QTimer::singleShot(0, this, &SqlScanResultProcessor::displayMessages); // do in the UI thread
0077 
0078     unblockUpdates();
0079 }
0080 
0081 void
0082 SqlScanResultProcessor::displayMessages()
0083 {
0084     QString errorList = m_messages.join( "</li><li>" ).replace( '\n', "<br>" );
0085     QString text = i18n( "<ul><li>%1</li></ul>"
0086                          "In most cases this means that not all of your tracks were imported.<br>"
0087                          "See <a href='http://userbase.kde.org/Amarok/Manual/Various/TroubleshootingAndCommonProblems#Duplicate_Tracks'>"
0088                          "Amarok Manual</a> for information about duplicate tracks.", errorList );
0089     KMessageBox::error( The::mainWindow(), text, i18n( "Errors During Collection Scan" ),
0090                         KMessageBox::AllowLink );
0091 
0092     m_messages.clear();
0093 }
0094 
0095 void
0096 SqlScanResultProcessor::blockUpdates()
0097 {
0098     DEBUG_BLOCK
0099 
0100     m_collection->blockUpdatedSignal();
0101     m_collection->registry()->blockDatabaseUpdate();
0102 }
0103 
0104 void
0105 SqlScanResultProcessor::unblockUpdates()
0106 {
0107     DEBUG_BLOCK
0108 
0109     m_collection->registry()->unblockDatabaseUpdate();
0110     m_collection->unblockUpdatedSignal();
0111 }
0112 
0113 void
0114 SqlScanResultProcessor::message( const QString& message )
0115 {
0116     m_messages.append( message );
0117 }
0118 
0119 void
0120 SqlScanResultProcessor::commitDirectory( QSharedPointer<CollectionScanner::Directory> directory )
0121 {
0122     QString path = directory->path();
0123     // a bit of paranoia:
0124     if( m_foundDirectories.contains( path ) )
0125         warning() << "commitDirectory(): duplicate directory path" << path << "in"
0126                   << "collectionscanner output. This shouldn't happen.";
0127 
0128     // getDirectory() updates the directory entry mtime:
0129     int dirId = m_collection->registry()->getDirectory( path, directory->mtime() );
0130     // we never dereference key of m_directoryIds, it is safe to add it as a plain pointer
0131     m_directoryIds.insert( directory.data(), dirId );
0132     m_foundDirectories.insert( path, dirId );
0133 
0134     AbstractScanResultProcessor::commitDirectory( directory );
0135 
0136     // --- unblock every 5 second. Maybe not really needed, but still nice
0137     if( m_blockedTime.secsTo( QDateTime::currentDateTime() ) >= 5 )
0138     {
0139         unblockUpdates();
0140         m_blockedTime = QDateTime::currentDateTime();
0141         blockUpdates();
0142     }
0143 }
0144 
0145 void
0146 SqlScanResultProcessor::commitAlbum( CollectionScanner::Album *album )
0147 {
0148     debug() << "commitAlbum on"<<album->name()<< "artist"<<album->artist();
0149 
0150     // --- get or create the album
0151     Meta::SqlAlbumPtr metaAlbum;
0152     metaAlbum = Meta::SqlAlbumPtr::staticCast( m_collection->getAlbum( album->name(), album->artist() ) );
0153     if( !metaAlbum )
0154         return;
0155     m_albumIds.insert( album, metaAlbum->id() );
0156 
0157     // --- add all tracks
0158     foreach( CollectionScanner::Track *track, album->tracks() )
0159         commitTrack( track, album );
0160 
0161     // --- set the cover if we have one
0162     // we need to do this after the tracks are added in case of an embedded cover
0163     bool suppressAutoFetch = metaAlbum->suppressImageAutoFetch();
0164     metaAlbum->setSuppressImageAutoFetch( true );
0165     if( m_type == GenericScanManager::FullScan )
0166     {
0167         if( !album->cover().isEmpty() )
0168         {
0169             metaAlbum->removeImage();
0170             metaAlbum->setImage( album->cover() );
0171         }
0172     }
0173     else
0174     {
0175         if( !metaAlbum->hasImage() && !album->cover().isEmpty() )
0176             metaAlbum->setImage( album->cover() );
0177     }
0178     metaAlbum->setSuppressImageAutoFetch( suppressAutoFetch );
0179 }
0180 
0181 void
0182 SqlScanResultProcessor::commitTrack( CollectionScanner::Track *track,
0183                                      CollectionScanner::Album *srcAlbum )
0184 {
0185     // debug() << "commitTrack on"<<track->title()<< "album"<<srcAlbum->name() << "dir:" << track->directory()->path()<<track->directory();
0186 
0187     Q_ASSERT( track );
0188     Q_ASSERT( srcAlbum );
0189 
0190     Q_ASSERT( m_directoryIds.contains( track->directory() ) );
0191     int directoryId = m_directoryIds.value( track->directory() );
0192     Q_ASSERT( m_albumIds.contains( srcAlbum ) );
0193     int albumId = m_albumIds.value( srcAlbum );
0194 
0195     QString uid = track->uniqueid();
0196     if( uid.isEmpty() )
0197     {
0198         warning() << "commitTrack(): got track with empty unique id from the scanner,"
0199                   << "not adding it";
0200         m_messages.append( QString( "Not adding track %1 because it has no unique id." ).
0201                              arg(track->path()) );
0202         return;
0203     }
0204     uid = m_collection->generateUidUrl( uid );
0205 
0206     int deviceId = m_collection->mountPointManager()->getIdForUrl( QUrl::fromUserInput(track->path()) );
0207     QString rpath = m_collection->mountPointManager()->getRelativePath( deviceId, track->path() );
0208 
0209     if( m_foundTracks.contains( uid ) )
0210     {
0211         const UrlEntry old = m_urlsCache.value( m_uidCache.value( uid ) );
0212         const char *pattern = I18N_NOOP( "Duplicates found, the second file will be ignored:\n%1\n%2" );
0213 
0214         // we want translated version for GUI and non-translated for debug log
0215         warning() << "commitTrack():" << QString( pattern ).arg( old.path, track->path() );
0216         m_messages.append( i18n( pattern, old.path, track->path() ) );
0217         return;
0218     }
0219 
0220     Meta::SqlTrackPtr metaTrack;
0221     UrlEntry entry;
0222     // find an existing track by uid
0223     if( m_uidCache.contains( uid ) )
0224     {
0225         // uid is sadly not unique. Try to find the best url id.
0226         int urlId = findBestUrlId( uid, track->path() );
0227         Q_ASSERT( urlId > 0 );
0228         Q_ASSERT( m_urlsCache.contains( urlId ) );
0229         entry = m_urlsCache.value( urlId );
0230         entry.path = track->path();
0231         entry.directoryId = directoryId;
0232 
0233         metaTrack = Meta::SqlTrackPtr::staticCast( m_collection->registry()->getTrack( urlId ) );
0234         Q_ASSERT( metaTrack->urlId() == entry.id );
0235     }
0236     // find an existing track by path
0237     else if( m_pathCache.contains( track->path() ) )
0238     {
0239         int urlId = m_pathCache.value( track->path() );
0240         Q_ASSERT( m_urlsCache.contains( urlId ) );
0241         entry = m_urlsCache.value( urlId );
0242         entry.uid = uid;
0243         entry.directoryId = directoryId;
0244 
0245         metaTrack = Meta::SqlTrackPtr::staticCast( m_collection->registry()->getTrack( urlId ) );
0246         Q_ASSERT( metaTrack->urlId() == entry.id );
0247     }
0248     // create a new one
0249     else
0250     {
0251         static int autoDecrementId = -1;
0252         entry.id = autoDecrementId--;
0253         entry.path = track->path();
0254         entry.uid = uid;
0255         entry.directoryId = directoryId;
0256 
0257         metaTrack = Meta::SqlTrackPtr::staticCast( m_collection->getTrack( deviceId, rpath, directoryId, uid ) );
0258     }
0259 
0260     if( !metaTrack )
0261     {
0262         QString text = QString( "Something went wrong when importing track %1, metaTrack "
0263                 "is null while it shouldn't be." ).arg( track->path() );
0264         warning() << "commitTrack():" << text.toLocal8Bit().data();
0265         m_messages.append( text );
0266         return;
0267     }
0268     urlsCacheInsert( entry ); // removes the previous entry (by id) first if necessary
0269     m_foundTracks.insert( uid, entry.id );
0270 
0271     // TODO: we need to check the modified date of the file against the last updated of the file
0272     // to figure out if the track information was updated from outside Amarok.
0273     // In such a case we would fully reread all the information as if in a FullScan
0274 
0275     // -- set the values
0276     metaTrack->setWriteFile( false ); // no need to write the tags back
0277     metaTrack->beginUpdate();
0278 
0279     metaTrack->setUidUrl( uid );
0280     metaTrack->setUrl( deviceId, rpath, directoryId );
0281 
0282     if( m_type == GenericScanManager::FullScan ||
0283         !track->title().isEmpty() )
0284         metaTrack->setTitle( track->title() );
0285 
0286     if( m_type == GenericScanManager::FullScan ||
0287         albumId != -1 )
0288         metaTrack->setAlbum( albumId );
0289 
0290     if( m_type == GenericScanManager::FullScan ||
0291         !track->artist().isEmpty() )
0292         metaTrack->setArtist( track->artist() );
0293 
0294     if( m_type == GenericScanManager::FullScan ||
0295         !track->composer().isEmpty() )
0296         metaTrack->setComposer( track->composer() );
0297 
0298     if( m_type == GenericScanManager::FullScan ||
0299         track->year() >= 0 )
0300         metaTrack->setYear( (track->year() >= 0) ? track->year() : 0 );
0301 
0302     if( m_type == GenericScanManager::FullScan ||
0303         !track->genre().isEmpty() )
0304         metaTrack->setGenre( track->genre() );
0305 
0306     metaTrack->setType( track->filetype() );
0307 
0308     if( m_type == GenericScanManager::FullScan ||
0309         track->bpm() >= 0 )
0310         metaTrack->setBpm( track->bpm() );
0311 
0312     if( m_type == GenericScanManager::FullScan ||
0313         !track->comment().isEmpty() )
0314         metaTrack->setComment( track->comment() );
0315 
0316     if( (m_type == GenericScanManager::FullScan || metaTrack->score() == 0) &&
0317         track->score() >= 0 )
0318         metaTrack->setScore( track->score() );
0319 
0320     if( (m_type == GenericScanManager::FullScan || metaTrack->rating() == 0.0) &&
0321         track->rating() >= 0 )
0322         metaTrack->setRating( track->rating() );
0323 
0324     if( (m_type == GenericScanManager::FullScan || metaTrack->length() == 0) &&
0325         track->length() >= 0 )
0326         metaTrack->setLength( track->length() );
0327 
0328     // the filesize is updated every time after the
0329     // file is changed. Doesn't make sense to set it.
0330 
0331     if( (m_type == GenericScanManager::FullScan || !metaTrack->modifyDate().isValid()) &&
0332         track->modified().isValid() )
0333         metaTrack->setModifyDate( track->modified() );
0334 
0335     if( (m_type == GenericScanManager::FullScan || metaTrack->sampleRate() == 0) &&
0336         track->samplerate() >= 0 )
0337         metaTrack->setSampleRate( track->samplerate() );
0338 
0339     if( (m_type == GenericScanManager::FullScan || metaTrack->bitrate() == 0) &&
0340         track->bitrate() >= 0 )
0341         metaTrack->setBitrate( track->bitrate() );
0342 
0343     if( (m_type == GenericScanManager::FullScan || metaTrack->trackNumber() == 0) &&
0344         track->track() >= 0 )
0345         metaTrack->setTrackNumber( track->track() );
0346 
0347     if( (m_type == GenericScanManager::FullScan || metaTrack->discNumber() == 0) &&
0348         track->disc() >= 0 )
0349         metaTrack->setDiscNumber( track->disc() );
0350 
0351     if( m_type == GenericScanManager::FullScan && track->playcount() >= metaTrack->playCount() )
0352         metaTrack->setPlayCount( track->playcount() );
0353 
0354 
0355     Meta::ReplayGainTag modes[] = { Meta::ReplayGain_Track_Gain,
0356         Meta::ReplayGain_Track_Peak,
0357         Meta::ReplayGain_Album_Gain,
0358         Meta::ReplayGain_Album_Peak };
0359 
0360     for( int i=0; i<4; i++ )
0361     {
0362         if( track->replayGain( modes[i] ) != 0.0 )
0363             metaTrack->setReplayGain( modes[i], track->replayGain( modes[i] ) );
0364     }
0365 
0366     metaTrack->endUpdate();
0367     metaTrack->setWriteFile( true );
0368 }
0369 
0370 void
0371 SqlScanResultProcessor::deleteDeletedDirectories()
0372 {
0373     auto storage = m_collection->sqlStorage();
0374 
0375     QList<DirectoryEntry> toCheck;
0376     switch( m_type )
0377     {
0378         case GenericScanManager::FullScan:
0379         case GenericScanManager::UpdateScan:
0380             toCheck = mountedDirectories();
0381             break;
0382         case GenericScanManager::PartialUpdateScan:
0383             toCheck = deletedDirectories();
0384     }
0385 
0386     // -- check if the have been found during the scan
0387     foreach( const DirectoryEntry &e, toCheck )
0388     {
0389         /* we need to match directories by their (absolute) path, otherwise following
0390          * scenario triggers statistics loss (bug 298275):
0391          *
0392          * 1. user relocates collection to different filesystem, but clones path structure
0393          *    or toggles MassStorageDeviceHandler enabled in Config -> plugins.
0394          * 2. collectionscanner knows nothings about directory ids, so it doesn't detect
0395          *    any track changes and emits a bunch of skipped (unchanged) dirs with no
0396          *    tracks.
0397          * 3. SqlRegistry::getDirectory() called there from returns different directory id
0398          *    then in past.
0399          * 4. deleteDeletedDirectories() is called, and if it operates on directory ids,
0400          *    it happily removes _all_ directories, taking tracks with it.
0401          * 5. Tracks disappear from the UI until full rescan, stats, lyrics, labels are
0402          *    lost forever.
0403          */
0404         QString path = m_collection->mountPointManager()->getAbsolutePath( e.deviceId, e.dir );
0405         bool deleteThisDir = false;
0406         if( !m_foundDirectories.contains( path ) )
0407             deleteThisDir = true;
0408         else if( m_foundDirectories.value( path ) != e.dirId )
0409         {
0410             int newDirId = m_foundDirectories.value( path );
0411             // as a safety measure, we don't delete the old dir if relocation fails
0412             deleteThisDir = relocateTracksToNewDirectory( e.dirId, newDirId );
0413         }
0414 
0415         if( deleteThisDir )
0416         {
0417             deleteDeletedTracks( e.dirId );
0418             QString query = QString( "DELETE FROM directories WHERE id = %1;" ).arg( e.dirId );
0419             storage->query( query );
0420         }
0421     }
0422 }
0423 
0424 void
0425 SqlScanResultProcessor::deleteDeletedTracksAndSubdirs( QSharedPointer<CollectionScanner::Directory> directory )
0426 {
0427     Q_ASSERT( m_directoryIds.contains( directory.data() ) );
0428     int directoryId = m_directoryIds.value( directory.data() );
0429     // only deletes tracks directly in this dir
0430     deleteDeletedTracks( directoryId );
0431     // trigger deletion of deleted subdirectories in deleteDeletedDirectories():
0432     m_scannedDirectoryIds.insert( directoryId );
0433 }
0434 
0435 void
0436 SqlScanResultProcessor::deleteDeletedTracks( int directoryId )
0437 {
0438     // -- find all tracks
0439     QList<int> urlIds = m_directoryCache.values( directoryId );
0440 
0441     // -- check if the tracks have been found during the scan
0442     foreach( int urlId, urlIds )
0443     {
0444         Q_ASSERT( m_urlsCache.contains( urlId ) );
0445         const UrlEntry &entry = m_urlsCache[ urlId ];
0446         Q_ASSERT( entry.directoryId == directoryId );
0447         // we need to match both uid and url id, because uid is not unique
0448         if( !m_foundTracks.contains( entry.uid, entry.id ) )
0449         {
0450             removeTrack( entry );
0451             urlsCacheRemove( entry );
0452         }
0453     }
0454 }
0455 
0456 int
0457 SqlScanResultProcessor::findBestUrlId( const QString &uid, const QString &path )
0458 {
0459     QList<int> urlIds = m_uidCache.values( uid );
0460     if( urlIds.isEmpty() )
0461         return -1;
0462     if( urlIds.size() == 1 )
0463         return urlIds.at( 0 ); // normal operation
0464 
0465     foreach( int testedUrlId, urlIds )
0466     {
0467         Q_ASSERT( m_urlsCache.contains( testedUrlId ) );
0468         if( m_urlsCache[ testedUrlId ].path == path )
0469             return testedUrlId;
0470     }
0471 
0472     warning() << "multiple url entries with uid" << uid << "found in the database, but"
0473               << "none with current path" << path << "Choosing blindly the last one out"
0474               << "of url id candidates" << urlIds;
0475     return urlIds.last();
0476 }
0477 
0478 bool
0479 SqlScanResultProcessor::relocateTracksToNewDirectory( int oldDirId, int newDirId )
0480 {
0481     QList<int> urlIds = m_directoryCache.values( oldDirId );
0482     if( urlIds.isEmpty() )
0483         return true; // nothing to do
0484 
0485     MountPointManager *manager = m_collection->mountPointManager();
0486     SqlRegistry *reg = m_collection->registry();
0487     auto storage = m_collection->sqlStorage();
0488 
0489     // sanity checking, not strictly needed, but imagine new device appearing in the
0490     // middle of the scan, so rather prevent db corruption:
0491     QStringList res = storage->query( QString( "SELECT deviceid FROM directories "
0492                                                "WHERE id = %1" ).arg( newDirId ) );
0493     if( res.count() != 1 )
0494     {
0495         warning() << "relocateTracksToNewDirectory(): no or multiple entries when"
0496                   << "querying directory with id" << newDirId;
0497         return false;
0498     }
0499     int newDirDeviceId = res.at( 0 ).toInt();
0500 
0501     foreach( int urlId, urlIds )
0502     {
0503         Q_ASSERT( m_urlsCache.contains( urlId ) );
0504         UrlEntry entry = m_urlsCache.value( urlId );
0505         Meta::SqlTrackPtr track = Meta::SqlTrackPtr::staticCast( reg->getTrack( urlId ) );
0506         Q_ASSERT( track );
0507 
0508         // not strictly needed, but we want to sanity check it to prevent corrupt db
0509         int deviceId = manager->getIdForUrl( QUrl::fromUserInput(entry.path) );
0510         if( newDirDeviceId != deviceId )
0511         {
0512             warning() << "relocateTracksToNewDirectory(): device id from newDirId ("
0513                       << res.at( 0 ).toInt() << ") and device id from mountPointManager ("
0514                       << deviceId << ") don't match!";
0515             return false;
0516         }
0517         QString rpath = manager->getRelativePath( deviceId, entry.path );
0518 
0519         track->setUrl( deviceId, rpath, newDirId );
0520         entry.directoryId = newDirId;
0521         urlsCacheInsert( entry ); // removes the previous entry (by id) first
0522     }
0523     return true;
0524 }
0525 
0526 void
0527 SqlScanResultProcessor::removeTrack( const UrlEntry &entry )
0528 {
0529     debug() << "removeTrack(" << entry << ")";
0530     // we used to skip track removal is m_messages wasn't empty, but that lead to to
0531     // tracks left laying around. We now hope that the result processor got better and
0532     // only removes tracks that should be removed.
0533 
0534     SqlRegistry *reg = m_collection->registry();
0535     // we must get the track by id, uid is not unique
0536     Meta::SqlTrackPtr track = Meta::SqlTrackPtr::staticCast( reg->getTrack( entry.id ) );
0537     Q_ASSERT( track->urlId() == entry.id );
0538     track->remove();
0539 }
0540 
0541 QList<SqlScanResultProcessor::DirectoryEntry>
0542 SqlScanResultProcessor::mountedDirectories() const
0543 {
0544     auto storage = m_collection->sqlStorage();
0545 
0546     // -- get a list of all mounted device ids
0547     QList<int> idList = m_collection->mountPointManager()->getMountedDeviceIds();
0548     QString deviceIds;
0549     foreach( int id, idList )
0550     {
0551         if ( !deviceIds.isEmpty() )
0552             deviceIds += ',';
0553         deviceIds += QString::number( id );
0554     }
0555 
0556     // -- get all (mounted) directories
0557     QString query = QString( "SELECT id, deviceid, dir FROM directories "
0558                              "WHERE deviceid IN (%1)" ).arg( deviceIds );
0559     QStringList res = storage->query( query );
0560 
0561     QList<DirectoryEntry> result;
0562     for( int i = 0; i < res.count(); )
0563     {
0564         DirectoryEntry e;
0565         e.dirId = res.at( i++ ).toInt();
0566         e.deviceId = res.at( i++ ).toInt();
0567         e.dir = res.at( i++ );
0568         result << e;
0569     }
0570 
0571     return result;
0572 }
0573 
0574 QList<SqlScanResultProcessor::DirectoryEntry>
0575 SqlScanResultProcessor::deletedDirectories() const
0576 {
0577     auto storage = m_collection->sqlStorage();
0578 
0579     QHash<int, DirectoryEntry> idToDirEntryMap; // for faster processing during filtering
0580     foreach( int directoryId, m_scannedDirectoryIds )
0581     {
0582         QString query = QString( "SELECT deviceid, dir FROM directories WHERE id = %1" )
0583                 .arg( directoryId );
0584         QStringList res = storage->query( query );
0585         if( res.count() != 2 )
0586         {
0587             warning() << "unexpected query result" << res << "in deletedDirectories()";
0588             continue;
0589         }
0590 
0591         int deviceId = res.at( 0 ).toInt();
0592         QString dir = res.at( 1 );
0593         // select all child directories
0594         query = QString( "SELECT id, deviceid, dir FROM directories WHERE deviceid = %1 "
0595                 "AND dir LIKE '%2_%'" ).arg( deviceId ).arg( storage->escape( dir ) );
0596         res = storage->query( query );
0597         for( int i = 0; i < res.count(); )
0598         {
0599             DirectoryEntry e;
0600             e.dirId = res.at( i++ ).toInt();
0601             e.deviceId = res.at( i++ ).toInt();
0602             e.dir = res.at( i++ );
0603             idToDirEntryMap.insert( e.dirId, e );
0604         }
0605     }
0606 
0607     // now we must filter out all found directories *and their children*, because the
0608     // children are *not* in m_foundDirectories and deleteDeletedDirectories() would
0609     // remove them erroneously
0610     foreach( int foundDirectoryId, m_foundDirectories )
0611     {
0612         if( idToDirEntryMap.contains( foundDirectoryId ) )
0613         {
0614             int existingDeviceId = idToDirEntryMap[ foundDirectoryId ].deviceId;
0615             QString existingPath = idToDirEntryMap[ foundDirectoryId ].dir;
0616             idToDirEntryMap.remove( foundDirectoryId );
0617 
0618             // now remove all children of the existing directory
0619             QMutableHashIterator<int, DirectoryEntry> it( idToDirEntryMap );
0620             while( it.hasNext() )
0621             {
0622                 const DirectoryEntry &e = it.next().value();
0623                 if( e.deviceId == existingDeviceId && e.dir.startsWith( existingPath ) )
0624                     it.remove();
0625             }
0626         }
0627     }
0628 
0629     return idToDirEntryMap.values();
0630 }
0631 
0632 void
0633 SqlScanResultProcessor::urlsCacheInit()
0634 {
0635     DEBUG_BLOCK
0636 
0637     auto storage = m_collection->sqlStorage();
0638 
0639     QString query = QStringLiteral( "SELECT id, deviceid, rpath, directory, uniqueid FROM urls;");
0640     QStringList res = storage->query( query );
0641 
0642     for( int i = 0; i < res.count(); )
0643     {
0644         int id = res.at(i++).toInt();
0645         int deviceId = res.at(i++).toInt();
0646         QString rpath = res.at(i++);
0647         int directoryId = res.at(i++).toInt();
0648         QString uid = res.at(i++);
0649 
0650         QString path;
0651         if( deviceId )
0652             path = m_collection->mountPointManager()->getAbsolutePath( deviceId, rpath );
0653         else
0654             path = rpath;
0655 
0656         UrlEntry entry;
0657         entry.id = id;
0658         entry.path = path;
0659         entry.directoryId = directoryId;
0660         entry.uid = uid;
0661 
0662         if( !directoryId )
0663         {
0664             warning() << "Found urls entry without directory. A phantom track. Removing" << path;
0665             removeTrack( entry );
0666             continue;
0667         }
0668 
0669         urlsCacheInsert( entry );
0670 
0671         QAbstractEventDispatcher::instance()->processEvents( QEventLoop::AllEvents );
0672     }
0673 }
0674 
0675 void
0676 SqlScanResultProcessor::urlsCacheInsert( const UrlEntry &entry )
0677 {
0678     // this case is normal operation
0679     if( m_urlsCache.contains( entry.id ) )
0680         urlsCacheRemove( m_urlsCache[ entry.id ] );
0681 
0682     // following shouldn't normally happen:
0683     if( m_pathCache.contains( entry.path ) )
0684     {
0685         int oldId = m_pathCache.value( entry.path );
0686         Q_ASSERT( m_urlsCache.contains( oldId ) );
0687         const UrlEntry &old = m_urlsCache[ oldId ];
0688         warning() << "urlsCacheInsert(): found duplicate in path. old" << old
0689                   << "will be hidden by the new one in the cache:" << entry;
0690     }
0691 
0692     // this will signify error in this class:
0693     Q_ASSERT( !m_uidCache.contains( entry.uid, entry.id ) );
0694     Q_ASSERT( !m_directoryCache.contains( entry.directoryId, entry.id ) );
0695 
0696     m_urlsCache.insert( entry.id, entry );
0697     m_uidCache.insert( entry.uid, entry.id );
0698     m_pathCache.insert( entry.path, entry.id );
0699     m_directoryCache.insert( entry.directoryId, entry.id );
0700 }
0701 
0702 void
0703 SqlScanResultProcessor::cleanupMembers()
0704 {
0705     m_foundDirectories.clear();
0706     m_foundTracks.clear();
0707     m_scannedDirectoryIds.clear();
0708     m_directoryIds.clear();
0709     m_albumIds.clear();
0710 
0711     m_urlsCache.clear();
0712     m_uidCache.clear();
0713     m_pathCache.clear();
0714     m_directoryCache.clear();
0715 
0716     AbstractScanResultProcessor::cleanupMembers();
0717 }
0718 
0719 void
0720 SqlScanResultProcessor::urlsCacheRemove( const UrlEntry &entry )
0721 {
0722     if( !m_urlsCache.contains( entry.id ) )
0723         return;
0724 
0725     m_uidCache.remove( entry.uid, entry.id );
0726     m_pathCache.remove( entry.path );
0727     m_directoryCache.remove( entry.directoryId, entry.id );
0728     m_urlsCache.remove( entry.id );
0729 }
0730 
0731 QDebug
0732 operator<<( QDebug dbg, const SqlScanResultProcessor::UrlEntry &entry )
0733 {
0734      dbg.nospace() << "Entry(id=" << entry.id << ", path=" << entry.path << ", dirId="
0735                    << entry.directoryId << ", uid=" << entry.uid << ")";
0736      return dbg.space();
0737 }