Fix list index out of bounds crash on loading of .pls playlists, thanks to Tim Beaule...
[amarok.git] / src / mountpointmanager.cpp
blob7f95c2fe29ae05b90a5b8da54aec20502b59d48a
1 /*
2 * Copyright (c) 2006-2007 Maximilian Kossick <maximilian.kossick@googlemail.com>
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 #define DEBUG_PREFIX "MountPointManager"
21 #include "mountpointmanager.h"
23 #include "amarok.h"
24 #include "collectiondb.h"
25 #include "debug.h"
26 #include "pluginmanager.h"
27 #include "ContextStatusBar.h"
29 //solid stuff
30 #include <solid/predicate.h>
31 #include <solid/device.h>
32 #include <solid/deviceinterface.h>
33 #include <solid/devicenotifier.h>
34 #include <solid/storagevolume.h>
36 #include <QFile>
37 #include <QList>
38 #include <QStringList>
39 #include <QTimer>
41 typedef Medium::List MediumList;
43 MountPointManager::MountPointManager()
44 : QObject( 0 )
46 setObjectName( "MountPointManager" );
48 if ( !Amarok::config( "Collection" ).readEntry( "DynamicCollection", true ) )
50 debug() << "Dynamic Collection deactivated in amarokrc, not loading plugins, not connecting signals";
51 return;
54 connect( Solid::DeviceNotifier::instance(), SIGNAL( deviceAdded( QString ) ), SLOT( deviceAdded( QString ) ) );
55 connect( Solid::DeviceNotifier::instance(), SIGNAL( deviceRemoved( QString ) ), SLOT( deviceRemoved( QString ) ) );
57 init();
59 CollectionDB *collDB = CollectionDB::instance();
61 if ( collDB->adminValue( "Database Stats Version" ).toInt() >= 9 && /* make sure that deviceid actually exists*/
62 collDB->query( "SELECT COUNT(url) FROM statistics WHERE deviceid = -2;" ).first().toInt() != 0 )
64 connect( this, SIGNAL( mediumConnected( int ) ), SLOT( migrateStatistics() ) );
65 QTimer::singleShot( 0, this, SLOT( migrateStatistics() ) );
67 connect( this, SIGNAL( mediumConnected( int ) ), SLOT( updateStatisticsURLs() ) );
68 updateStatisticsURLs();
72 MountPointManager::~MountPointManager()
74 m_handlerMapMutex.lock();
75 foreach( DeviceHandler *dh, m_handlerMap )
77 delete dh;
79 while( !m_mediumFactories.isEmpty() )
80 delete m_mediumFactories.takeFirst();
81 while( !m_remoteFactories.isEmpty() )
82 delete m_remoteFactories.takeFirst();
83 m_handlerMapMutex.unlock();
86 MountPointManager * MountPointManager::instance( )
88 static MountPointManager instance;
89 return &instance;
92 void
93 MountPointManager::init()
95 DEBUG_BLOCK
96 KService::List plugins = PluginManager::query( "[X-KDE-Amarok-plugintype] == 'device'" );
97 debug() << "Received [" << QString::number( plugins.count() ) << "] device plugin offers";
98 oldForeachType( KService::List, plugins )
100 Amarok::Plugin *plugin = PluginManager::createFromService( *it );
101 if( plugin )
103 DeviceHandlerFactory *factory = static_cast<DeviceHandlerFactory*>( plugin );
104 if ( factory->canCreateFromMedium() )
105 m_mediumFactories.append( factory );
106 else if (factory->canCreateFromConfig() )
107 m_remoteFactories.append( factory );
108 else
109 //FIXME max: better error message
110 debug() << "Unknown DeviceHandlerFactory";
112 else debug() << "Plugin could not be loaded";
117 MountPointManager::getIdForUrl( const KUrl &url )
119 int mountPointLength = 0;
120 int id = -1;
121 m_handlerMapMutex.lock();
122 foreach( DeviceHandler *dh, m_handlerMap )
124 if ( url.path().startsWith( dh->getDevicePath() ) && mountPointLength < dh->getDevicePath().length() )
126 id = m_handlerMap.key( dh );
127 mountPointLength = dh->getDevicePath().length();
130 m_handlerMapMutex.unlock();
131 if ( mountPointLength > 0 )
133 return id;
135 else
137 //default fallback if we could not identify the mount point.
138 //treat -1 as mount point / in all other methods
139 return -1;
144 MountPointManager::getIdForUrl( const QString &url )
146 return getIdForUrl( KUrl( url ) );
149 bool
150 MountPointManager::isMounted ( const int deviceId ) const {
151 m_handlerMapMutex.lock();
152 bool result = m_handlerMap.contains( deviceId );
153 m_handlerMapMutex.unlock();
154 return result;
157 QString
158 MountPointManager::getMountPointForId( const int id ) const
160 QString mountPoint;
161 if ( isMounted( id ) )
163 m_handlerMapMutex.lock();
164 mountPoint = m_handlerMap[id]->getDevicePath();
165 m_handlerMapMutex.unlock();
167 else
168 //TODO better error handling
169 mountPoint = "/";
170 return mountPoint;
173 void
174 MountPointManager::getAbsolutePath( const int deviceId, const KUrl& relativePath, KUrl& absolutePath) const
176 //debug() << "id is " << deviceId << ", relative path is " << relativePath.path();
177 if ( deviceId == -1 )
179 absolutePath.setPath( "/" );
180 absolutePath.addPath( relativePath.path() );
181 absolutePath.cleanPath();
182 //debug() << "Deviceid is -1, using relative Path as absolute Path, returning " << absolutePath.path();
183 return;
185 m_handlerMapMutex.lock();
186 if ( m_handlerMap.contains( deviceId ) )
188 m_handlerMap[deviceId]->getURL( absolutePath, relativePath );
189 m_handlerMapMutex.unlock();
191 else
193 m_handlerMapMutex.unlock();
194 QStringList lastMountPoint = CollectionDB::instance()->query(
195 QString( "SELECT lastmountpoint FROM devices WHERE id = %1" )
196 .arg( deviceId ) );
197 if ( lastMountPoint.count() == 0 )
199 //hmm, no device with that id in the DB...serious problem
200 absolutePath.setPath( "/" );
201 absolutePath.addPath( relativePath.path() );
202 absolutePath.cleanPath();
203 warning() << "Device " << deviceId << " not in database, this should never happen! Returning " << absolutePath.path();
205 else
207 absolutePath.setPath( lastMountPoint.first() );
208 absolutePath.addPath( relativePath.path() );
209 absolutePath.cleanPath();
210 // debug() << "Device " << deviceId << " not mounted, using last mount point and returning " << absolutePath.path();
215 QString
216 MountPointManager::getAbsolutePath( const int deviceId, const QString& relativePath ) const
218 KUrl rpath;
219 rpath.setPath( relativePath );
220 KUrl url;
221 getAbsolutePath( deviceId, rpath, url );
222 return url.path();
225 void
226 MountPointManager::getRelativePath( const int deviceId, const KUrl& absolutePath, KUrl& relativePath ) const
228 m_handlerMapMutex.lock();
229 if ( deviceId != -1 && m_handlerMap.contains( deviceId ) )
231 //FIXME max: returns garbage if the absolute path is actually not under the device's mount point
232 QString rpath = KUrl::relativePath( m_handlerMap[deviceId]->getDevicePath(), absolutePath.path() );
233 m_handlerMapMutex.unlock();
234 relativePath.setPath( rpath );
236 else
238 m_handlerMapMutex.unlock();
239 //TODO: better error handling
240 QString rpath = KUrl::relativePath( "/", absolutePath.path() );
241 relativePath.setPath( rpath );
245 QString
246 MountPointManager::getRelativePath( const int deviceId, const QString& absolutePath ) const
248 KUrl url;
249 getRelativePath( deviceId, KUrl( absolutePath ), url );
250 return url.path();
253 void
254 MountPointManager::mediumChanged( const Medium *m )
256 DEBUG_BLOCK
257 if ( !m ) return;
258 if ( m->isMounted() )
260 foreach( DeviceHandlerFactory *factory, m_mediumFactories )
262 if ( factory->canHandle ( m ) )
264 debug() << "found handler for " << m->id();
265 DeviceHandler *handler = factory->createHandler( m );
266 if( !handler )
268 debug() << "Factory " << factory->type() << "could not create device handler";
269 break;
271 int key = handler->getDeviceID();
272 m_handlerMapMutex.lock();
273 if ( m_handlerMap.contains( key ) )
275 debug() << "Key " << key << " already exists in handlerMap, replacing";
276 delete m_handlerMap[key];
277 m_handlerMap.remove( key );
279 m_handlerMap.insert( key, handler );
280 m_handlerMapMutex.unlock();
281 debug() << "added device " << key << " with mount point " << m->mountPoint();
282 emit mediumConnected( key );
283 break; //we found the added medium and don't have to check the other device handlers
287 else
289 m_handlerMapMutex.lock();
290 foreach( DeviceHandler *dh, m_handlerMap )
292 if ( dh->deviceIsMedium( m ) )
294 int key = m_handlerMap.key( dh );
295 m_handlerMap.remove( key );
296 delete dh;
297 debug() << "removed device " << key;
298 m_handlerMapMutex.unlock();
299 emit mediumRemoved( key );
300 //we found the medium which was removed, so we can abort the loop
301 return;
304 m_handlerMapMutex.unlock();
308 void
309 MountPointManager::mediumRemoved( const Medium *m )
311 DEBUG_BLOCK
312 if ( !m )
314 //reinit?
316 else
318 //this works for USB devices, special cases might be required for other devices
319 m_handlerMapMutex.lock();
320 foreach( DeviceHandler *dh, m_handlerMap )
322 if( dh->deviceIsMedium( m ) )
324 int key = m_handlerMap.key( dh );
325 m_handlerMap.remove( key );
326 delete dh;
327 debug() << "removed device " << key;
328 m_handlerMapMutex.unlock();
329 emit mediumRemoved( key );
330 //we found the medium which was removed, so we can abort the loop
331 return;
334 m_handlerMapMutex.unlock();
338 void
339 MountPointManager::mediumAdded( const Medium *m )
341 DEBUG_BLOCK
342 if ( !m ) return;
343 if ( m->isMounted() )
345 debug() << "Device added and mounted, checking handlers";
346 foreach( DeviceHandlerFactory *factory, m_mediumFactories )
348 if( factory->canHandle( m ) )
350 debug() << "found handler for " << m->id();
351 DeviceHandler *handler = factory->createHandler( m );
352 if( !handler )
354 debug() << "Factory " << factory->type() << "could not create device handler";
355 break;
357 int key = handler->getDeviceID();
358 m_handlerMapMutex.lock();
359 if( m_handlerMap.contains( key ) )
361 debug() << "Key " << key << " already exists in handlerMap, replacing";
362 delete m_handlerMap[key];
363 m_handlerMap.remove( key );
365 m_handlerMap.insert( key, handler );
366 m_handlerMapMutex.unlock();
367 debug() << "added device " << key << " with mount point " << m->mountPoint();
368 emit mediumConnected( key );
369 break; //we found the added medium and don't have to check the other device handlers
375 IdList
376 MountPointManager::getMountedDeviceIds() const {
377 m_handlerMapMutex.lock();
378 IdList list( m_handlerMap.keys() );
379 m_handlerMapMutex.unlock();
380 list.append( -1 );
381 return list;
384 QStringList
385 MountPointManager::collectionFolders( )
387 //TODO max: cache data
388 QStringList result;
389 KConfigGroup folders = Amarok::config( "Collection Folders" );
390 IdList ids = getMountedDeviceIds();
391 foreach( int id, ids )
393 QStringList rpaths = folders.readEntry( QString::number( id ), QStringList() );
394 foreach( const QString &strIt, rpaths )
396 QString absPath;
397 if ( strIt == "./" )
399 absPath = getMountPointForId( id );
401 else
403 absPath = getAbsolutePath( id, strIt );
405 if ( !result.contains( absPath ) )
406 result.append( absPath );
409 return result;
412 void
413 MountPointManager::setCollectionFolders( const QStringList &folders )
415 //TODO max: cache data
416 typedef QMap<int, QStringList> FolderMap;
417 KConfigGroup folderConf = Amarok::config( "Collection Folders" );
418 FolderMap folderMap;
420 foreach( const QString &folder, folders )
422 int id = getIdForUrl( folder );
423 QString rpath = getRelativePath( id, folder );
424 if ( folderMap.contains( id ) ) {
425 if ( !folderMap[id].contains( rpath ) )
426 folderMap[id].append( rpath );
428 else
429 folderMap[id] = QStringList( rpath );
431 //make sure that collection folders on devices which are not in foldermap are deleted
432 IdList ids = getMountedDeviceIds();
433 foreach( int deviceId, ids )
435 if( !folderMap.contains( deviceId ) )
437 folderConf.deleteEntry( QString::number( deviceId ) );
440 QMapIterator<int, QStringList> i( folderMap );
441 while( i.hasNext() )
443 i.next();
444 folderConf.writeEntry( QString::number( i.key() ), i.value() );
448 void
449 MountPointManager::migrateStatistics()
451 QStringList urls = CollectionDB::instance()->query( "SELECT url FROM statistics WHERE deviceid = -2;" );
452 foreach( const QString &url, urls )
454 if ( QFile::exists( url ) )
456 int deviceid = getIdForUrl( url );
457 QString rpath = getRelativePath( deviceid, url );
458 QString update = QString( "UPDATE statistics SET deviceid = %1, url = '%2'" )
459 .arg( deviceid )
460 .arg( CollectionDB::instance()->escapeString( rpath ) );
461 update += QString( " WHERE url = '%1' AND deviceid = -2;" )
462 .arg( CollectionDB::instance()->escapeString( url ) );
463 CollectionDB::instance()->query( update );
468 void
469 MountPointManager::updateStatisticsURLs( bool changed )
471 if ( changed )
472 QTimer::singleShot( 0, this, SLOT( startStatisticsUpdateJob() ) );
475 void
476 MountPointManager::startStatisticsUpdateJob()
478 ThreadManager::instance()->queueJob( new UrlUpdateJob( this ) );
481 void
482 MountPointManager::checkDeviceAvailability()
484 //code to actively scan for devices which are not supported by KDE mediamanager should go here
485 //method is not actually called yet
488 void
489 MountPointManager::deviceAdded( const QString &udi )
491 Solid::Predicate predicate = Solid::Predicate( Solid::DeviceInterface::StorageVolume, "udi", udi );
492 QList<Solid::Device> devices = Solid::Device::listFromQuery( predicate );
493 //there'll be maximum one device because we are using the udi in the predicate
494 if( !devices.isEmpty() )
496 Solid::StorageVolume *volume = devices[0].as<Solid::StorageVolume>();
497 Q_UNUSED( volume );
501 void
502 MountPointManager::deviceRemoved( const QString &udi )
504 Q_UNUSED( udi );
507 //UrlUpdateJob
509 bool UrlUpdateJob::doJob( )
511 DEBUG_BLOCK
512 updateStatistics();
513 updateLabels();
514 return true;
517 void UrlUpdateJob::updateStatistics( )
519 CollectionDB *collDB = CollectionDB::instance();
520 MountPointManager *mpm = MountPointManager::instance();
521 QStringList urls = collDB->query( "SELECT s.deviceid,s.url "
522 "FROM statistics AS s LEFT JOIN tags AS t ON s.deviceid = t.deviceid AND s.url = t.url "
523 "WHERE t.url IS NULL AND s.deviceid != -2;" );
524 debug() << "Trying to update " << urls.count() / 2 << " statistics rows";
525 oldForeach( urls )
527 int deviceid = (*it).toInt();
528 QString rpath = *++it;
529 QString realURL = mpm->getAbsolutePath( deviceid, rpath );
530 if( QFile::exists( realURL ) )
532 int newDeviceid = mpm->getIdForUrl( realURL );
533 if( newDeviceid == deviceid )
534 continue;
535 QString newRpath = mpm->getRelativePath( newDeviceid, realURL );
537 int statCount = collDB->query(
538 QString( "SELECT COUNT( url ) FROM statistics WHERE deviceid = %1 AND url = '%2';" )
539 .arg( newDeviceid )
540 .arg( collDB->escapeString( newRpath ) ) ).first().toInt();
541 if( statCount )
542 continue; //statistics row with new URL/deviceid values already exists
544 QString sql = QString( "UPDATE statistics SET deviceid = %1, url = '%2'" )
545 .arg( newDeviceid ).arg( collDB->escapeString( newRpath ) );
546 sql += QString( " WHERE deviceid = %1 AND url = '%2';" )
547 .arg( deviceid ).arg( collDB->escapeString( rpath ) );
548 collDB->query( sql );
553 void UrlUpdateJob::updateLabels( )
555 CollectionDB *collDB = CollectionDB::instance();
556 MountPointManager *mpm = MountPointManager::instance();
557 QStringList labels = collDB->query( "SELECT l.deviceid,l.url "
558 "FROM tags_labels AS l LEFT JOIN tags as t ON l.deviceid = t.deviceid AND l.url = t.url "
559 "WHERE t.url IS NULL;" );
560 debug() << "Trying to update " << labels.count() / 2 << " tags_labels rows";
561 oldForeach( labels )
563 int deviceid = (*it).toInt();
564 QString rpath = *++it;
565 QString realUrl = mpm->getAbsolutePath( deviceid, rpath );
566 if( QFile::exists( realUrl ) )
568 int newDeviceid = mpm->getIdForUrl( realUrl );
569 if( newDeviceid == deviceid )
570 continue;
571 QString newRpath = mpm->getRelativePath( newDeviceid, realUrl );
573 //only update rows if there is not already a row with the new deviceid/rpath and the same labelid
574 QStringList labelids = collDB->query(
575 QString( "SELECT labelid FROM tags_labels WHERE deviceid = %1 AND url = '%2';" )
576 .arg( QString::number( newDeviceid ), collDB->escapeString( newRpath ) ) );
577 QString existingLabelids;
578 if( !labelids.isEmpty() )
580 existingLabelids = " AND labelid NOT IN (";
581 oldForeach( labelids )
583 if( it != labelids.begin() )
584 existingLabelids += ',';
585 existingLabelids += *it;
587 existingLabelids += ')';
589 QString sql = QString( "UPDATE tags_labels SET deviceid = %1, url = '%2' "
590 "WHERE deviceid = %3 AND url = '%4'%5;" )
591 .arg( newDeviceid )
592 .arg( collDB->escapeString( newRpath ),
593 QString::number( deviceid ),
594 collDB->escapeString( rpath ),
595 existingLabelids );
596 collDB->query( sql );
601 #include "mountpointmanager.moc"