1 /****************************************************************************************
2  * Copyright (c) 2012 Matěj Laitl <matej@laitl.cz>                                      *
3  *                                                                                      *
4  * This program is free software; you can redistribute it and/or modify it under        *
5  * the terms of the GNU General Public License as published by the Free Software        *
6  * Foundation; either version 2 of the License, or (at your option) any later           *
7  * version.                                                                             *
8  *                                                                                      *
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
10  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
11  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
12  *                                                                                      *
13  * You should have received a copy of the GNU General Public License along with         *
14  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
15  ****************************************************************************************/
16 
17 #include "IpodCollection.h"
18 
19 #include "IpodCollectionLocation.h"
20 #include "IpodMeta.h"
21 #include "IpodPlaylistProvider.h"
22 #include "jobs/IpodWriteDatabaseJob.h"
23 #include "jobs/IpodParseTracksJob.h"
24 #include "support/IphoneMountPoint.h"
25 #include "support/IpodDeviceHelper.h"
26 #include "support/IpodTranscodeCapability.h"
27 
28 #include "core/capabilities/ActionsCapability.h"
29 #include "core/logger/Logger.h"
30 #include "core/support/Components.h"
31 #include "core/support/Debug.h"
32 #include "core-impl/collections/support/MemoryCollection.h"
33 #include "core-impl/collections/support/MemoryMeta.h"
34 #include "core-impl/collections/support/MemoryQueryMaker.h"
35 #include "playlistmanager/PlaylistManager.h"
36 
37 #include <KDiskFreeSpaceInfo>
38 #include <solid/device.h>
39 #include <solid/predicate.h>
40 #include <solid/storageaccess.h>
41 #include <ThreadWeaver/Queue>
42 
43 #include <QTemporaryFile>
44 #include <QWeakPointer>
45 
46 #include <gpod/itdb.h>
47 #include <KConfigGroup>
48 #include <QDialogButtonBox>
49 #include <QPushButton>
50 #include <QVBoxLayout>
51 
52 
53 const QString IpodCollection::s_uidUrlProtocol = QString( "amarok-ipodtrackuid" );
54 const QStringList IpodCollection::s_audioFileTypes = QStringList() << "mp3" << "aac"
55     << "m4a" /* MPEG-4 AAC and also ALAC */ << "m4b" /* audiobook */ << "aiff" << "wav";
56 const QStringList IpodCollection::s_videoFileTypes = QStringList() << "m4v" << "mov";
57 const QStringList IpodCollection::s_audioVideoFileTypes = QStringList() << "mp4";
58 
IpodCollection(const QDir & mountPoint,const QString & uuid)59 IpodCollection::IpodCollection( const QDir &mountPoint, const QString &uuid )
60     : Collections::Collection()
61     , m_configureDialog( 0 )
62     , m_mc( new Collections::MemoryCollection() )
63     , m_itdb( 0 )
64     , m_lastUpdated( 0 )
65     , m_preventUnmountTempFile( 0 )
66     , m_mountPoint( mountPoint.absolutePath() )
67     , m_uuid( uuid )
68     , m_iphoneAutoMountpoint( 0 )
69     , m_playlistProvider( 0 )
70     , m_configureAction( 0 )
71     , m_ejectAction( 0 )
72     , m_consolidateAction( 0 )
73 {
74     DEBUG_BLOCK
75     if( m_uuid.isEmpty() )
76         m_uuid = m_mountPoint;
77 }
78 
IpodCollection(const QString & uuid)79 IpodCollection::IpodCollection( const QString &uuid )
80     : Collections::Collection()
81     , m_configureDialog( 0 )
82     , m_mc( new Collections::MemoryCollection() )
83     , m_itdb( 0 )
84     , m_lastUpdated( 0 )
85     , m_preventUnmountTempFile( 0 )
86     , m_uuid( uuid )
87     , m_playlistProvider( 0 )
88     , m_configureAction( 0 )
89     , m_ejectAction( 0 )
90     , m_consolidateAction( 0 )
91 {
92     DEBUG_BLOCK
93     // following constructor displays sorry message if it cannot mount iPhone:
94     m_iphoneAutoMountpoint = new IphoneMountPoint( uuid );
95     m_mountPoint = m_iphoneAutoMountpoint->mountPoint();
96     if( m_uuid.isEmpty() )
97         m_uuid = m_mountPoint;
98 }
99 
init()100 bool IpodCollection::init()
101 {
102     if( m_mountPoint.isEmpty() )
103         return false;  // we have already displayed sorry message
104 
105     m_updateTimer.setSingleShot( true );
106     connect( this, &IpodCollection::startUpdateTimer, this, &IpodCollection::slotStartUpdateTimer );
107     connect( &m_updateTimer, &QTimer::timeout, this, &IpodCollection::collectionUpdated );
108 
109     m_writeDatabaseTimer.setSingleShot( true );
110     connect( this, &IpodCollection::startWriteDatabaseTimer, this, &IpodCollection::slotStartWriteDatabaseTimer );
111     connect( &m_writeDatabaseTimer, &QTimer::timeout, this, &IpodCollection::slotInitiateDatabaseWrite );
112 
113     m_configureAction = new QAction( QIcon::fromTheme( "configure" ), i18n( "&Configure Device" ), this );
114     m_configureAction->setProperty( "popupdropper_svg_id", "configure" );
115     connect( m_configureAction, &QAction::triggered, this, &IpodCollection::slotShowConfigureDialog );
116 
117     m_ejectAction = new QAction( QIcon::fromTheme( "media-eject" ), i18n( "&Eject Device" ), this );
118     m_ejectAction->setProperty( "popupdropper_svg_id", "eject" );
119     connect( m_ejectAction, &QAction::triggered, this, &IpodCollection::slotEject );
120 
121     QString parseErrorMessage;
122     m_itdb = IpodDeviceHelper::parseItdb( m_mountPoint, parseErrorMessage );
123     m_prettyName = IpodDeviceHelper::collectionName( m_itdb ); // allows null m_itdb
124 
125     // m_consolidateAction is used by the provider
126     m_consolidateAction = new QAction( QIcon::fromTheme( "dialog-ok-apply" ), i18n( "Re-add orphaned and forget stale tracks" ), this );
127     // provider needs to be up before IpodParseTracksJob is started
128     m_playlistProvider = new IpodPlaylistProvider( this );
129     connect( m_playlistProvider, &IpodPlaylistProvider::startWriteDatabaseTimer, this, &IpodCollection::startWriteDatabaseTimer );
130     connect( m_consolidateAction, &QAction::triggered, m_playlistProvider, &IpodPlaylistProvider::slotConsolidateStaleOrphaned );
131     The::playlistManager()->addProvider( m_playlistProvider, m_playlistProvider->category() );
132 
133     if( m_itdb )
134     {
135         // parse tracks in a thread in order not to block main thread
136         IpodParseTracksJob *job = new IpodParseTracksJob( this );
137         m_parseTracksJob = job;
138         connect( job, &IpodParseTracksJob::done, job, &QObject::deleteLater );
139         ThreadWeaver::Queue::instance()->enqueue( QSharedPointer<ThreadWeaver::Job>(job) );
140     }
141     else
142         slotShowConfigureDialogWithError( parseErrorMessage ); // shows error message and allows initializing
143 
144     return true;  // we have found iPod, even if it might not be initialised
145 }
146 
~IpodCollection()147 IpodCollection::~IpodCollection()
148 {
149     DEBUG_BLOCK
150     The::playlistManager()->removeProvider( m_playlistProvider );
151 
152     // this is not racy: destructor should be called in a main thread, the timer fires in the
153     // same thread
154     if( m_writeDatabaseTimer.isActive() )
155     {
156         m_writeDatabaseTimer.stop();
157         // call directly from main thread in destructor, we have no other chance:
158         writeDatabase();
159     }
160     delete m_preventUnmountTempFile; // this should have been certainly 0, but why not
161     m_preventUnmountTempFile = 0;
162 
163     /* because m_itdb takes ownership of the tracks added to it, we need to remove the
164      * tracks from itdb before we delete it because in Amarok, IpodMeta::Track is the owner
165      * of the track */
166     IpodDeviceHelper::unlinkPlaylistsTracksFromItdb( m_itdb );  // does nothing if m_itdb is null
167     itdb_free( m_itdb );  // does nothing if m_itdb is null
168     m_itdb = 0;
169 
170     delete m_configureDialog;
171     delete m_iphoneAutoMountpoint; // this can unmount iPhone and remove temporary dir
172 }
173 
174 bool
possiblyContainsTrack(const QUrl & url) const175 IpodCollection::possiblyContainsTrack( const QUrl &url ) const
176 {
177     return url.toLocalFile().startsWith( m_mountPoint );
178 }
179 
180 Meta::TrackPtr
trackForUrl(const QUrl & url)181 IpodCollection::trackForUrl( const QUrl &url )
182 {
183     QString relativePath = url.toLocalFile().mid( m_mountPoint.size() + 1 );
184     QString uidUrl = QString( "%1/%2" ).arg( collectionId(), relativePath );
185     return trackForUidUrl( uidUrl );
186 }
187 
188 bool
hasCapabilityInterface(Capabilities::Capability::Type type) const189 IpodCollection::hasCapabilityInterface( Capabilities::Capability::Type type ) const
190 {
191     switch( type )
192     {
193         case Capabilities::Capability::Actions:
194         case Capabilities::Capability::Transcode:
195             return true;
196         default:
197             break;
198     }
199     return false;
200 }
201 
202 Capabilities::Capability*
createCapabilityInterface(Capabilities::Capability::Type type)203 IpodCollection::createCapabilityInterface( Capabilities::Capability::Type type )
204 {
205     switch( type )
206     {
207         case Capabilities::Capability::Actions:
208         {
209             QList<QAction *> actions;
210             if( m_configureAction )
211                 actions << m_configureAction;
212             if( m_ejectAction )
213                 actions << m_ejectAction;
214             if( m_consolidateAction && m_playlistProvider && m_playlistProvider->hasStaleOrOrphaned() )
215                 actions << m_consolidateAction;
216             return new Capabilities::ActionsCapability( actions );
217         }
218         case Capabilities::Capability::Transcode:
219         {
220             gchar *deviceDirChar = itdb_get_device_dir( QFile::encodeName( m_mountPoint ) );
221             QString deviceDir = QFile::decodeName( deviceDirChar );
222             g_free( deviceDirChar );
223             return new Capabilities::IpodTranscodeCapability( this, deviceDir );
224         }
225         default:
226             break;
227     }
228     return 0;
229 }
230 
231 Collections::QueryMaker*
queryMaker()232 IpodCollection::queryMaker()
233 {
234     return new Collections::MemoryQueryMaker( m_mc.toWeakRef(), collectionId() );
235 }
236 
237 QString
uidUrlProtocol() const238 IpodCollection::uidUrlProtocol() const
239 {
240     return s_uidUrlProtocol;
241 }
242 
243 QString
collectionId() const244 IpodCollection::collectionId() const
245 {
246     return QStringLiteral( "%1://%2" ).arg( s_uidUrlProtocol, m_uuid );
247 }
248 
249 QString
prettyName() const250 IpodCollection::prettyName() const
251 {
252     return m_prettyName;
253 }
254 
255 QIcon
icon() const256 IpodCollection::icon() const
257 {
258     return QIcon::fromTheme("multimedia-player-apple-ipod");
259 }
260 
261 bool
hasCapacity() const262 IpodCollection::hasCapacity() const
263 {
264     return KDiskFreeSpaceInfo::freeSpaceInfo( m_mountPoint ).isValid();
265 }
266 
267 float
usedCapacity() const268 IpodCollection::usedCapacity() const
269 {
270     return KDiskFreeSpaceInfo::freeSpaceInfo( m_mountPoint ).used();
271 }
272 
273 float
totalCapacity() const274 IpodCollection::totalCapacity() const
275 {
276     return KDiskFreeSpaceInfo::freeSpaceInfo( m_mountPoint ).size();
277 }
278 
279 Collections::CollectionLocation*
location()280 IpodCollection::location()
281 {
282     return new IpodCollectionLocation( QPointer<IpodCollection>( this ) );
283 }
284 
285 bool
isWritable() const286 IpodCollection::isWritable() const
287 {
288     return IpodDeviceHelper::safeToWrite( m_mountPoint, m_itdb ); // returns false if m_itdb is null
289 }
290 
291 bool
isOrganizable() const292 IpodCollection::isOrganizable() const
293 {
294     return false; // iPods are never organizable
295 }
296 
297 void
metadataChanged(const Meta::TrackPtr & track)298 IpodCollection::metadataChanged(const Meta::TrackPtr &track )
299 {
300     // reflect change to outside world:
301     bool mapsChanged = MemoryMeta::MapChanger( m_mc.data() ).trackChanged( track );
302     if( mapsChanged )
303         // while docs say something different, collection browser doesn't update unless we Q_EMIT updated()
304         Q_EMIT startUpdateTimer();
305     Q_EMIT startWriteDatabaseTimer();
306 }
307 
308 QString
mountPoint()309 IpodCollection::mountPoint()
310 {
311     return m_mountPoint;
312 }
313 
314 float
capacityMargin() const315 IpodCollection::capacityMargin() const
316 {
317     return 20*1024*1024; // 20 MiB
318 }
319 
320 QStringList
supportedFormats() const321 IpodCollection::supportedFormats() const
322 {
323     QStringList ret( s_audioFileTypes );
324     if( m_itdb && itdb_device_supports_video( m_itdb->device ) )
325         ret << s_videoFileTypes << s_audioVideoFileTypes;
326     return ret;
327 }
328 
329 Playlists::UserPlaylistProvider*
playlistProvider() const330 IpodCollection::playlistProvider() const
331 {
332     return m_playlistProvider;
333 }
334 
335 Meta::TrackPtr
trackForUidUrl(const QString & uidUrl)336 IpodCollection::trackForUidUrl( const QString &uidUrl )
337 {
338     m_mc->acquireReadLock();
339     Meta::TrackPtr ret = m_mc->trackMap().value( uidUrl, Meta::TrackPtr() );
340     m_mc->releaseLock();
341     return ret;
342 }
343 
344 void
slotDestroy()345 IpodCollection::slotDestroy()
346 {
347     // guard against user hitting the button twice or hitting it while there is another
348     // write database job already running
349     if( m_writeDatabaseJob )
350     {
351         IpodWriteDatabaseJob *job = m_writeDatabaseJob.data();
352         // don't create duplicate connections:
353         disconnect( job, &QObject::destroyed, this, &IpodCollection::slotRemove );
354         disconnect( job, &QObject::destroyed, this, &IpodCollection::slotPerformTeardownAndRemove );
355         connect( job, &QObject::destroyed, this, &IpodCollection::slotRemove );
356     }
357     // this is not racy: slotDestroy() is delivered to main thread, the timer fires in the
358     // same thread
359     else if( m_writeDatabaseTimer.isActive() )
360     {
361         // write database in a thread so that it need not be written in destructor
362         m_writeDatabaseTimer.stop();
363         IpodWriteDatabaseJob *job = new IpodWriteDatabaseJob( this );
364         m_writeDatabaseJob = job;
365         connect( job, &IpodWriteDatabaseJob::done, job, &QObject::deleteLater );
366         connect( job, &QObject::destroyed, this, &IpodCollection::slotRemove );
367         ThreadWeaver::Queue::instance()->enqueue( QSharedPointer<ThreadWeaver::Job>(job) );
368     }
369     else
370         slotRemove();
371 }
372 
373 void
slotEject()374 IpodCollection::slotEject()
375 {
376     // guard against user hitting the button twice or hitting it while there is another
377     // write database job already running
378     if( m_writeDatabaseJob )
379     {
380         IpodWriteDatabaseJob *job = m_writeDatabaseJob.data();
381         // don't create duplicate connections:
382         disconnect( job, &QObject::destroyed, this, &IpodCollection::slotRemove );
383         disconnect( job, &QObject::destroyed, this, &IpodCollection::slotPerformTeardownAndRemove );
384         connect( job, &QObject::destroyed, this, &IpodCollection::slotPerformTeardownAndRemove );
385     }
386     // this is not racy: slotEject() is delivered to main thread, the timer fires in the
387     // same thread
388     else if( m_writeDatabaseTimer.isActive() )
389     {
390         // write database now because iPod will be already unmounted in destructor
391         m_writeDatabaseTimer.stop();
392         IpodWriteDatabaseJob *job = new IpodWriteDatabaseJob( this );
393         m_writeDatabaseJob = job;
394         connect( job, &IpodWriteDatabaseJob::done, job, &QObject::deleteLater );
395         connect( job, &QObject::destroyed, this, &IpodCollection::slotPerformTeardownAndRemove );
396         ThreadWeaver::Queue::instance()->enqueue( QSharedPointer<ThreadWeaver::Job>(job) );
397     }
398     else
399         slotPerformTeardownAndRemove();
400 }
401 
402 void
slotShowConfigureDialog()403 IpodCollection::slotShowConfigureDialog()
404 {
405     slotShowConfigureDialogWithError( QString() );
406 }
407 
408 void
slotShowConfigureDialogWithError(const QString & errorMessage)409 IpodCollection::slotShowConfigureDialogWithError( const QString &errorMessage )
410 {
411     if( !m_configureDialog )
412     {
413         // create the dialog
414         m_configureDialog = new QDialog();
415         QWidget *settingsWidget = new QWidget( m_configureDialog );
416         m_configureDialogUi.setupUi( settingsWidget );
417 
418         QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel);
419         QWidget *mainWidget = new QWidget;
420         QVBoxLayout *mainLayout = new QVBoxLayout;
421         m_configureDialog->setLayout(mainLayout);
422         mainLayout->addWidget(mainWidget);
423         QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
424         okButton->setDefault(true);
425         okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
426         connect(buttonBox, &QDialogButtonBox::accepted, m_configureDialog, &QDialog::accept);
427         connect(buttonBox, &QDialogButtonBox::rejected, m_configureDialog, &QDialog::reject);
428 
429         mainLayout->addWidget(settingsWidget);
430         mainLayout->addWidget(buttonBox);
431 
432         m_configureDialog->setWindowTitle( settingsWidget->windowTitle() );  // setupUi() sets this
433         if( m_itdb )
434         {
435             // we will never initialize this iPod this time, hide ui for it completely
436             m_configureDialogUi.modelComboLabel->hide();
437             m_configureDialogUi.modelComboBox->hide();
438             m_configureDialogUi.initializeLabel->hide();
439             m_configureDialogUi.initializeButton->hide();
440         }
441 
442         connect( m_configureDialogUi.initializeButton, &QPushButton::clicked, this, &IpodCollection::slotInitialize );
443         connect( m_configureDialog, &QDialog::accepted, this, &IpodCollection::slotApplyConfiguration );
444     }
445     QScopedPointer<Capabilities::TranscodeCapability> tc( create<Capabilities::TranscodeCapability>() );
446     IpodDeviceHelper::fillInConfigureDialog( m_configureDialog, &m_configureDialogUi,
447                                              m_mountPoint, m_itdb, tc->savedConfiguration(),
448                                              errorMessage );
449 
450     // don't allow to resize the dialog too small:
451     m_configureDialog->setMinimumSize( m_configureDialog->sizeHint() );
452     m_configureDialog->show();
453     m_configureDialog->raise();
454 }
455 
collectionUpdated()456 void IpodCollection::collectionUpdated()
457 {
458     m_lastUpdated = QDateTime::currentMSecsSinceEpoch();
459     Q_EMIT updated();
460 }
461 
462 void
slotInitialize()463 IpodCollection::slotInitialize()
464 {
465     if( m_itdb )
466         return;  // why the hell we were called?
467 
468     m_configureDialogUi.initializeButton->setEnabled( false );
469     QString errorMessage;
470     bool success = IpodDeviceHelper::initializeIpod( m_mountPoint, &m_configureDialogUi, errorMessage );
471     if( !success )
472     {
473         slotShowConfigureDialogWithError( errorMessage );
474         return;
475     }
476 
477     errorMessage.clear();
478     m_itdb = IpodDeviceHelper::parseItdb( m_mountPoint, errorMessage );
479     m_prettyName = IpodDeviceHelper::collectionName( m_itdb ); // allows null m_itdb
480 
481     if( m_itdb )
482     {
483         QScopedPointer<Capabilities::TranscodeCapability> tc( create<Capabilities::TranscodeCapability>() );
484         errorMessage = i18nc( "iPod was successfully initialized", "Initialization successful." );
485         // so that the buttons are re-enabled, info filled etc:
486         IpodDeviceHelper::fillInConfigureDialog( m_configureDialog, &m_configureDialogUi,
487             m_mountPoint, m_itdb, tc->savedConfiguration(), errorMessage );
488 
489         // there will be probably 0 tracks, but it may do more in future, for example stale
490         // & orphaned track search.
491         IpodParseTracksJob *job = new IpodParseTracksJob( this );
492         connect( job, &IpodParseTracksJob::done, job, &QObject::deleteLater );
493         ThreadWeaver::Queue::instance()->enqueue( QSharedPointer<ThreadWeaver::Job>(job) );
494     }
495     else
496         slotShowConfigureDialogWithError( errorMessage ); // shows error message and allows initializing
497 }
498 
499 void
slotApplyConfiguration()500 IpodCollection::slotApplyConfiguration()
501 {
502     if( !isWritable() )
503         return; // we can do nothing if we are not writeable
504 
505     QString newName = m_configureDialogUi.nameLineEdit->text();
506     if( !newName.isEmpty() && newName != IpodDeviceHelper::ipodName( m_itdb ) )
507     {
508         IpodDeviceHelper::setIpodName( m_itdb, newName );
509         m_prettyName = IpodDeviceHelper::collectionName( m_itdb );
510         Q_EMIT startWriteDatabaseTimer(); // the change should be written down to the database
511         Q_EMIT startUpdateTimer();
512     }
513 
514     QScopedPointer<Capabilities::TranscodeCapability> tc( create<Capabilities::TranscodeCapability>() );
515     tc->setSavedConfiguration( m_configureDialogUi.transcodeComboBox->currentChoice() );
516 }
517 
518 void
slotStartUpdateTimer()519 IpodCollection::slotStartUpdateTimer()
520 {
521     // there are no concurrency problems, this method can only be called from the main
522     // thread and that's where the timer fires
523     if( m_updateTimer.isActive() )
524         return; // already running, nothing to do
525 
526     // number of milliseconds to next desired update, may be negative
527     int timeout = m_lastUpdated + 1000 - QDateTime::currentMSecsSinceEpoch();
528     // give at least 50 msecs to catch multi-tracks edits nicely on the first frame
529     m_updateTimer.start( qBound( 50, timeout, 1000 ) );
530 }
531 
532 void
slotStartWriteDatabaseTimer()533 IpodCollection::slotStartWriteDatabaseTimer()
534 {
535     m_writeDatabaseTimer.start( 30000 );
536     // ensure we have a file on iPod open that prevents unmounting it if db is dirty
537     if( !m_preventUnmountTempFile )
538     {
539         m_preventUnmountTempFile = new QTemporaryFile();
540         QString name( "/.itunes_database_dirty_in_amarok_prevent_unmounting" );
541         m_preventUnmountTempFile->setFileTemplate( m_mountPoint + name );
542         m_preventUnmountTempFile->open();
543     }
544 }
545 
slotInitiateDatabaseWrite()546 void IpodCollection::slotInitiateDatabaseWrite()
547 {
548     if( m_writeDatabaseJob )
549     {
550         warning() << __PRETTY_FUNCTION__ << "called while m_writeDatabaseJob still points"
551                   << "to an older job. Not doing anything.";
552         return;
553     }
554     IpodWriteDatabaseJob *job = new IpodWriteDatabaseJob( this );
555     m_writeDatabaseJob = job;
556     connect( job, &IpodWriteDatabaseJob::done, job, &QObject::deleteLater );
557     ThreadWeaver::Queue::instance()->enqueue( QSharedPointer<ThreadWeaver::Job>(job) );
558 }
559 
slotPerformTeardownAndRemove()560 void IpodCollection::slotPerformTeardownAndRemove()
561 {
562     /* try to eject the device from system. Following technique potentially catches more
563      * cases than simply passing the udi from IpodCollectionFactory, think of fuse-based
564      * filesystems for mounting iPhones et cetera.. */
565     Solid::Predicate query( Solid::DeviceInterface::StorageAccess, QString( "filePath" ),
566                             m_mountPoint );
567     QList<Solid::Device> devices = Solid::Device::listFromQuery( query );
568     if( devices.count() == 1 )
569     {
570         Solid::Device device = devices.at( 0 );
571         Solid::StorageAccess *ssa = device.as<Solid::StorageAccess>();
572         if( ssa )
573             ssa->teardown();
574     }
575 
576     slotRemove();
577 }
578 
slotRemove()579 void IpodCollection::slotRemove()
580 {
581     // this is not racy, we are in the main thread and parseTracksJob can be deleted only
582     // in the main thread
583     if( m_parseTracksJob )
584     {
585         // we need to wait until parseTracksJob finishes, because it accesses IpodCollection
586         // and IpodPlaylistProvider in an asynchronous way that cannot safely cope with
587         // IpodCollection disappearing
588         connect( m_parseTracksJob.data(), &QObject::destroyed, this, &IpodCollection::remove );
589         m_parseTracksJob->abort();
590     }
591     else
592         Q_EMIT remove();
593 }
594 
595 Meta::TrackPtr
addTrack(IpodMeta::Track * track)596 IpodCollection::addTrack( IpodMeta::Track *track )
597 {
598     if( !track || !m_itdb )
599         return Meta::TrackPtr();
600 
601     Itdb_Track *itdbTrack = track->itdbTrack();
602     bool justAdded = false;
603 
604     m_itdbMutex.lock();
605     Q_ASSERT( !itdbTrack->itdb || itdbTrack->itdb == m_itdb /* refuse to take track from another itdb */ );
606     if( !itdbTrack->itdb )
607     {
608         itdb_track_add( m_itdb, itdbTrack, -1 );
609         // if it wasn't in itdb, it couldn't have legally been in master playlist
610         // TODO: podcasts should not go into MPL
611         itdb_playlist_add_track( itdb_playlist_mpl( m_itdb ), itdbTrack, -1 );
612 
613         justAdded = true;
614         Q_EMIT startWriteDatabaseTimer();
615     }
616     track->setCollection( QPointer<IpodCollection>( this ) );
617 
618     Meta::TrackPtr trackPtr( track );
619     Meta::TrackPtr memTrack = MemoryMeta::MapChanger( m_mc.data() ).addTrack( trackPtr );
620     if( !memTrack && justAdded )
621     {
622         /* this new track was not added to MemoryCollection, it may vanish soon, prevent
623          * dangling pointer in m_itdb */
624         itdb_playlist_remove_track( 0 /* = MPL */, itdbTrack );
625         itdb_track_unlink( itdbTrack );
626     }
627     m_itdbMutex.unlock();
628 
629     if( memTrack )
630     {
631         subscribeTo( trackPtr );
632         Q_EMIT startUpdateTimer();
633     }
634     return memTrack;
635 }
636 
637 void
removeTrack(const Meta::TrackPtr & track)638 IpodCollection::removeTrack( const Meta::TrackPtr &track )
639 {
640     if( !track )
641         return; // nothing to do
642     /* Following call ensures thread-safety even when this method is called multiple times
643      * from different threads with the same track: only one thread will get non-null
644      * deletedTrack from MapChanger. */
645     Meta::TrackPtr deletedTrack = MemoryMeta::MapChanger( m_mc.data() ).removeTrack( track );
646     if( !deletedTrack )
647     {
648         warning() << __PRETTY_FUNCTION__ << "attempt to delete a track that was not in"
649                   << "MemoryCollection or not added using MapChanger";
650         return;
651     }
652     IpodMeta::Track *ipodTrack = dynamic_cast<IpodMeta::Track *>( deletedTrack.data() );
653     if( !ipodTrack )
654     {
655         warning() << __PRETTY_FUNCTION__ << "attempt to delete a track that was not"
656                   << "internally iPod track";
657         return;
658     }
659 
660     Itdb_Track *itdbTrack = ipodTrack->itdbTrack();
661     if( itdbTrack->itdb && m_itdb )
662     {
663         // remove from all playlists excluding the MPL:
664         m_playlistProvider->removeTrackFromPlaylists( track );
665 
666         QMutexLocker locker( &m_itdbMutex );
667         // remove track from the master playlist:
668         itdb_playlist_remove_track( itdb_playlist_mpl( m_itdb ), itdbTrack );
669         // remove it from the db:
670         itdb_track_unlink( itdbTrack );
671         Q_EMIT startWriteDatabaseTimer();
672     }
673 
674     Q_EMIT startUpdateTimer();
675 }
676 
writeDatabase()677 bool IpodCollection::writeDatabase()
678 {
679     if( !IpodDeviceHelper::safeToWrite( m_mountPoint, m_itdb ) ) // returns false if m_itdb is null
680     {
681         // we have to delete unmount-preventing file even in this case
682         delete m_preventUnmountTempFile;
683         m_preventUnmountTempFile = 0;
684         warning() << "Refusing to write iTunes database to iPod becauase device is not safe to write";
685         return false;
686     }
687 
688     m_itdbMutex.lock();
689     GError *error = 0;
690     bool success = itdb_write( m_itdb, &error );
691     m_itdbMutex.unlock();
692     QString gpodError;
693     if( error )
694     {
695         gpodError = QString::fromUtf8( error->message );
696         g_error_free( error );
697         error = 0;
698     }
699     delete m_preventUnmountTempFile;  // this deletes the file
700     m_preventUnmountTempFile = 0;
701 
702     if( success )
703     {
704         QString message = i18nc( "%1: iPod collection name",
705                          "iTunes database successfully written to %1", prettyName() );
706         Amarok::Logger::shortMessage( message );
707     }
708     else
709     {
710         QString message;
711         if( gpodError.isEmpty() )
712             message = i18nc( "%1: iPod collection name",
713                              "Writing iTunes database to %1 failed without an indication of error",
714                              prettyName() );
715         else
716             message = i18nc( "%1: iPod collection name, %2: technical error from libgpod",
717                              "Writing iTunes database to %1 failed: %2", prettyName(), gpodError );
718         Amarok::Logger::longMessage( message );
719     }
720     return success;
721 }
722 
723