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