File indexing completed on 2025-01-05 04:26:00

0001 /****************************************************************************************
0002  * Copyright (c) 2012 Matěj Laitl <matej@laitl.cz>                                      *
0003  *                                                                                      *
0004  * This program is free software; you can redistribute it and/or modify it under        *
0005  * the terms of the GNU General Public License as published by the Free Software        *
0006  * Foundation; either version 2 of the License, or (at your option) any later           *
0007  * version.                                                                             *
0008  *                                                                                      *
0009  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0010  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0011  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0012  *                                                                                      *
0013  * You should have received a copy of the GNU General Public License along with         *
0014  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0015  ****************************************************************************************/
0016 
0017 #include "IpodCollection.h"
0018 
0019 #include "IpodCollectionLocation.h"
0020 #include "IpodMeta.h"
0021 #include "IpodPlaylistProvider.h"
0022 #include "jobs/IpodWriteDatabaseJob.h"
0023 #include "jobs/IpodParseTracksJob.h"
0024 #include "support/IphoneMountPoint.h"
0025 #include "support/IpodDeviceHelper.h"
0026 #include "support/IpodTranscodeCapability.h"
0027 
0028 #include "core/capabilities/ActionsCapability.h"
0029 #include "core/logger/Logger.h"
0030 #include "core/support/Components.h"
0031 #include "core/support/Debug.h"
0032 #include "core-impl/collections/support/MemoryCollection.h"
0033 #include "core-impl/collections/support/MemoryMeta.h"
0034 #include "core-impl/collections/support/MemoryQueryMaker.h"
0035 #include "playlistmanager/PlaylistManager.h"
0036 
0037 #include <KDiskFreeSpaceInfo>
0038 #include <solid/device.h>
0039 #include <solid/predicate.h>
0040 #include <solid/storageaccess.h>
0041 #include <ThreadWeaver/Queue>
0042 
0043 #include <QTemporaryFile>
0044 #include <QWeakPointer>
0045 
0046 #include <gpod/itdb.h>
0047 #include <KConfigGroup>
0048 #include <QDialogButtonBox>
0049 #include <QPushButton>
0050 #include <QVBoxLayout>
0051 
0052 
0053 const QString IpodCollection::s_uidUrlProtocol = QString( "amarok-ipodtrackuid" );
0054 const QStringList IpodCollection::s_audioFileTypes = QStringList() << "mp3" << "aac"
0055     << "m4a" /* MPEG-4 AAC and also ALAC */ << "m4b" /* audiobook */ << "aiff" << "wav";
0056 const QStringList IpodCollection::s_videoFileTypes = QStringList() << "m4v" << "mov";
0057 const QStringList IpodCollection::s_audioVideoFileTypes = QStringList() << "mp4";
0058 
0059 IpodCollection::IpodCollection( const QDir &mountPoint, const QString &uuid )
0060     : Collections::Collection()
0061     , m_configureDialog( nullptr )
0062     , m_mc( new Collections::MemoryCollection() )
0063     , m_itdb( nullptr )
0064     , m_lastUpdated( 0 )
0065     , m_preventUnmountTempFile( nullptr )
0066     , m_mountPoint( mountPoint.absolutePath() )
0067     , m_uuid( uuid )
0068     , m_iphoneAutoMountpoint( nullptr )
0069     , m_playlistProvider( nullptr )
0070     , m_configureAction( nullptr )
0071     , m_ejectAction( nullptr )
0072     , m_consolidateAction( nullptr )
0073 {
0074     DEBUG_BLOCK
0075     if( m_uuid.isEmpty() )
0076         m_uuid = m_mountPoint;
0077 }
0078 
0079 IpodCollection::IpodCollection( const QString &uuid )
0080     : Collections::Collection()
0081     , m_configureDialog( nullptr )
0082     , m_mc( new Collections::MemoryCollection() )
0083     , m_itdb( nullptr )
0084     , m_lastUpdated( 0 )
0085     , m_preventUnmountTempFile( nullptr )
0086     , m_uuid( uuid )
0087     , m_playlistProvider( nullptr )
0088     , m_configureAction( nullptr )
0089     , m_ejectAction( nullptr )
0090     , m_consolidateAction( nullptr )
0091 {
0092     DEBUG_BLOCK
0093     // following constructor displays error message if it cannot mount iPhone:
0094     m_iphoneAutoMountpoint = new IphoneMountPoint( uuid );
0095     m_mountPoint = m_iphoneAutoMountpoint->mountPoint();
0096     if( m_uuid.isEmpty() )
0097         m_uuid = m_mountPoint;
0098 }
0099 
0100 bool IpodCollection::init()
0101 {
0102     if( m_mountPoint.isEmpty() )
0103         return false;  // we have already displayed error message
0104 
0105     m_updateTimer.setSingleShot( true );
0106     connect( this, &IpodCollection::startUpdateTimer, this, &IpodCollection::slotStartUpdateTimer );
0107     connect( &m_updateTimer, &QTimer::timeout, this, &IpodCollection::collectionUpdated );
0108 
0109     m_writeDatabaseTimer.setSingleShot( true );
0110     connect( this, &IpodCollection::startWriteDatabaseTimer, this, &IpodCollection::slotStartWriteDatabaseTimer );
0111     connect( &m_writeDatabaseTimer, &QTimer::timeout, this, &IpodCollection::slotInitiateDatabaseWrite );
0112 
0113     m_configureAction = new QAction( QIcon::fromTheme( "configure" ), i18n( "&Configure Device" ), this );
0114     m_configureAction->setProperty( "popupdropper_svg_id", "configure" );
0115     connect( m_configureAction, &QAction::triggered, this, &IpodCollection::slotShowConfigureDialog );
0116 
0117     m_ejectAction = new QAction( QIcon::fromTheme( "media-eject" ), i18n( "&Eject Device" ), this );
0118     m_ejectAction->setProperty( "popupdropper_svg_id", "eject" );
0119     connect( m_ejectAction, &QAction::triggered, this, &IpodCollection::slotEject );
0120 
0121     QString parseErrorMessage;
0122     m_itdb = IpodDeviceHelper::parseItdb( m_mountPoint, parseErrorMessage );
0123     m_prettyName = IpodDeviceHelper::collectionName( m_itdb ); // allows null m_itdb
0124 
0125     // m_consolidateAction is used by the provider
0126     m_consolidateAction = new QAction( QIcon::fromTheme( "dialog-ok-apply" ), i18n( "Re-add orphaned and forget stale tracks" ), this );
0127     // provider needs to be up before IpodParseTracksJob is started
0128     m_playlistProvider = new IpodPlaylistProvider( this );
0129     connect( m_playlistProvider, &IpodPlaylistProvider::startWriteDatabaseTimer, this, &IpodCollection::startWriteDatabaseTimer );
0130     connect( m_consolidateAction, &QAction::triggered, m_playlistProvider, &IpodPlaylistProvider::slotConsolidateStaleOrphaned );
0131     The::playlistManager()->addProvider( m_playlistProvider, m_playlistProvider->category() );
0132 
0133     if( m_itdb )
0134     {
0135         // parse tracks in a thread in order not to block main thread
0136         IpodParseTracksJob *job = new IpodParseTracksJob( this );
0137         m_parseTracksJob = job;
0138         connect( job, &IpodParseTracksJob::done, job, &QObject::deleteLater );
0139         ThreadWeaver::Queue::instance()->enqueue( QSharedPointer<ThreadWeaver::Job>(job) );
0140     }
0141     else
0142         slotShowConfigureDialogWithError( parseErrorMessage ); // shows error message and allows initializing
0143 
0144     return true;  // we have found iPod, even if it might not be initialised
0145 }
0146 
0147 IpodCollection::~IpodCollection()
0148 {
0149     DEBUG_BLOCK
0150     The::playlistManager()->removeProvider( m_playlistProvider );
0151 
0152     // this is not racy: destructor should be called in a main thread, the timer fires in the
0153     // same thread
0154     if( m_writeDatabaseTimer.isActive() )
0155     {
0156         m_writeDatabaseTimer.stop();
0157         // call directly from main thread in destructor, we have no other chance:
0158         writeDatabase();
0159     }
0160     delete m_preventUnmountTempFile; // this should have been certainly 0, but why not
0161     m_preventUnmountTempFile = nullptr;
0162 
0163     /* because m_itdb takes ownership of the tracks added to it, we need to remove the
0164      * tracks from itdb before we delete it because in Amarok, IpodMeta::Track is the owner
0165      * of the track */
0166     IpodDeviceHelper::unlinkPlaylistsTracksFromItdb( m_itdb );  // does nothing if m_itdb is null
0167     itdb_free( m_itdb );  // does nothing if m_itdb is null
0168     m_itdb = nullptr;
0169 
0170     delete m_configureDialog;
0171     delete m_iphoneAutoMountpoint; // this can unmount iPhone and remove temporary dir
0172 }
0173 
0174 bool
0175 IpodCollection::possiblyContainsTrack( const QUrl &url ) const
0176 {
0177     return url.toLocalFile().startsWith( m_mountPoint );
0178 }
0179 
0180 Meta::TrackPtr
0181 IpodCollection::trackForUrl( const QUrl &url )
0182 {
0183     QString relativePath = url.toLocalFile().mid( m_mountPoint.size() + 1 );
0184     QString uidUrl = QString( "%1/%2" ).arg( collectionId(), relativePath );
0185     return trackForUidUrl( uidUrl );
0186 }
0187 
0188 bool
0189 IpodCollection::hasCapabilityInterface( Capabilities::Capability::Type type ) const
0190 {
0191     switch( type )
0192     {
0193         case Capabilities::Capability::Actions:
0194         case Capabilities::Capability::Transcode:
0195             return true;
0196         default:
0197             break;
0198     }
0199     return false;
0200 }
0201 
0202 Capabilities::Capability*
0203 IpodCollection::createCapabilityInterface( Capabilities::Capability::Type type )
0204 {
0205     switch( type )
0206     {
0207         case Capabilities::Capability::Actions:
0208         {
0209             QList<QAction *> actions;
0210             if( m_configureAction )
0211                 actions << m_configureAction;
0212             if( m_ejectAction )
0213                 actions << m_ejectAction;
0214             if( m_consolidateAction && m_playlistProvider && m_playlistProvider->hasStaleOrOrphaned() )
0215                 actions << m_consolidateAction;
0216             return new Capabilities::ActionsCapability( actions );
0217         }
0218         case Capabilities::Capability::Transcode:
0219         {
0220             gchar *deviceDirChar = itdb_get_device_dir( QFile::encodeName( m_mountPoint ) );
0221             QString deviceDir = QFile::decodeName( deviceDirChar );
0222             g_free( deviceDirChar );
0223             return new Capabilities::IpodTranscodeCapability( this, deviceDir );
0224         }
0225         default:
0226             break;
0227     }
0228     return nullptr;
0229 }
0230 
0231 Collections::QueryMaker*
0232 IpodCollection::queryMaker()
0233 {
0234     return new Collections::MemoryQueryMaker( m_mc.toWeakRef(), collectionId() );
0235 }
0236 
0237 QString
0238 IpodCollection::uidUrlProtocol() const
0239 {
0240     return s_uidUrlProtocol;
0241 }
0242 
0243 QString
0244 IpodCollection::collectionId() const
0245 {
0246     return QStringLiteral( "%1://%2" ).arg( s_uidUrlProtocol, m_uuid );
0247 }
0248 
0249 QString
0250 IpodCollection::prettyName() const
0251 {
0252     return m_prettyName;
0253 }
0254 
0255 QIcon
0256 IpodCollection::icon() const
0257 {
0258     return QIcon::fromTheme("multimedia-player-apple-ipod");
0259 }
0260 
0261 bool
0262 IpodCollection::hasCapacity() const
0263 {
0264     return KDiskFreeSpaceInfo::freeSpaceInfo( m_mountPoint ).isValid();
0265 }
0266 
0267 float
0268 IpodCollection::usedCapacity() const
0269 {
0270     return KDiskFreeSpaceInfo::freeSpaceInfo( m_mountPoint ).used();
0271 }
0272 
0273 float
0274 IpodCollection::totalCapacity() const
0275 {
0276     return KDiskFreeSpaceInfo::freeSpaceInfo( m_mountPoint ).size();
0277 }
0278 
0279 Collections::CollectionLocation*
0280 IpodCollection::location()
0281 {
0282     return new IpodCollectionLocation( QPointer<IpodCollection>( this ) );
0283 }
0284 
0285 bool
0286 IpodCollection::isWritable() const
0287 {
0288     return IpodDeviceHelper::safeToWrite( m_mountPoint, m_itdb ); // returns false if m_itdb is null
0289 }
0290 
0291 bool
0292 IpodCollection::isOrganizable() const
0293 {
0294     return false; // iPods are never organizable
0295 }
0296 
0297 void
0298 IpodCollection::metadataChanged(const Meta::TrackPtr &track )
0299 {
0300     // reflect change to outside world:
0301     bool mapsChanged = MemoryMeta::MapChanger( m_mc.data() ).trackChanged( track );
0302     if( mapsChanged )
0303         // while docs say something different, collection browser doesn't update unless we Q_EMIT updated()
0304         Q_EMIT startUpdateTimer();
0305     Q_EMIT startWriteDatabaseTimer();
0306 }
0307 
0308 QString
0309 IpodCollection::mountPoint()
0310 {
0311     return m_mountPoint;
0312 }
0313 
0314 float
0315 IpodCollection::capacityMargin() const
0316 {
0317     return 20*1024*1024; // 20 MiB
0318 }
0319 
0320 QStringList
0321 IpodCollection::supportedFormats() const
0322 {
0323     QStringList ret( s_audioFileTypes );
0324     if( m_itdb && itdb_device_supports_video( m_itdb->device ) )
0325         ret << s_videoFileTypes << s_audioVideoFileTypes;
0326     return ret;
0327 }
0328 
0329 Playlists::UserPlaylistProvider*
0330 IpodCollection::playlistProvider() const
0331 {
0332     return m_playlistProvider;
0333 }
0334 
0335 Meta::TrackPtr
0336 IpodCollection::trackForUidUrl( const QString &uidUrl )
0337 {
0338     m_mc->acquireReadLock();
0339     Meta::TrackPtr ret = m_mc->trackMap().value( uidUrl, Meta::TrackPtr() );
0340     m_mc->releaseLock();
0341     return ret;
0342 }
0343 
0344 void
0345 IpodCollection::slotDestroy()
0346 {
0347     // guard against user hitting the button twice or hitting it while there is another
0348     // write database job already running
0349     if( m_writeDatabaseJob )
0350     {
0351         IpodWriteDatabaseJob *job = m_writeDatabaseJob.data();
0352         // don't create duplicate connections:
0353         disconnect( job, &QObject::destroyed, this, &IpodCollection::slotRemove );
0354         disconnect( job, &QObject::destroyed, this, &IpodCollection::slotPerformTeardownAndRemove );
0355         connect( job, &QObject::destroyed, this, &IpodCollection::slotRemove );
0356     }
0357     // this is not racy: slotDestroy() is delivered to main thread, the timer fires in the
0358     // same thread
0359     else if( m_writeDatabaseTimer.isActive() )
0360     {
0361         // write database in a thread so that it need not be written in destructor
0362         m_writeDatabaseTimer.stop();
0363         IpodWriteDatabaseJob *job = new IpodWriteDatabaseJob( this );
0364         m_writeDatabaseJob = job;
0365         connect( job, &IpodWriteDatabaseJob::done, job, &QObject::deleteLater );
0366         connect( job, &QObject::destroyed, this, &IpodCollection::slotRemove );
0367         ThreadWeaver::Queue::instance()->enqueue( QSharedPointer<ThreadWeaver::Job>(job) );
0368     }
0369     else
0370         slotRemove();
0371 }
0372 
0373 void
0374 IpodCollection::slotEject()
0375 {
0376     // guard against user hitting the button twice or hitting it while there is another
0377     // write database job already running
0378     if( m_writeDatabaseJob )
0379     {
0380         IpodWriteDatabaseJob *job = m_writeDatabaseJob.data();
0381         // don't create duplicate connections:
0382         disconnect( job, &QObject::destroyed, this, &IpodCollection::slotRemove );
0383         disconnect( job, &QObject::destroyed, this, &IpodCollection::slotPerformTeardownAndRemove );
0384         connect( job, &QObject::destroyed, this, &IpodCollection::slotPerformTeardownAndRemove );
0385     }
0386     // this is not racy: slotEject() is delivered to main thread, the timer fires in the
0387     // same thread
0388     else if( m_writeDatabaseTimer.isActive() )
0389     {
0390         // write database now because iPod will be already unmounted in destructor
0391         m_writeDatabaseTimer.stop();
0392         IpodWriteDatabaseJob *job = new IpodWriteDatabaseJob( this );
0393         m_writeDatabaseJob = job;
0394         connect( job, &IpodWriteDatabaseJob::done, job, &QObject::deleteLater );
0395         connect( job, &QObject::destroyed, this, &IpodCollection::slotPerformTeardownAndRemove );
0396         ThreadWeaver::Queue::instance()->enqueue( QSharedPointer<ThreadWeaver::Job>(job) );
0397     }
0398     else
0399         slotPerformTeardownAndRemove();
0400 }
0401 
0402 void
0403 IpodCollection::slotShowConfigureDialog()
0404 {
0405     slotShowConfigureDialogWithError( QString() );
0406 }
0407 
0408 void
0409 IpodCollection::slotShowConfigureDialogWithError( const QString &errorMessage )
0410 {
0411     if( !m_configureDialog )
0412     {
0413         // create the dialog
0414         m_configureDialog = new QDialog();
0415         QWidget *settingsWidget = new QWidget( m_configureDialog );
0416         m_configureDialogUi.setupUi( settingsWidget );
0417 
0418         QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel);
0419         QWidget *mainWidget = new QWidget;
0420         QVBoxLayout *mainLayout = new QVBoxLayout;
0421         m_configureDialog->setLayout(mainLayout);
0422         mainLayout->addWidget(mainWidget);
0423         QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
0424         okButton->setDefault(true);
0425         okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
0426         connect(buttonBox, &QDialogButtonBox::accepted, m_configureDialog, &QDialog::accept);
0427         connect(buttonBox, &QDialogButtonBox::rejected, m_configureDialog, &QDialog::reject);
0428 
0429         mainLayout->addWidget(settingsWidget);
0430         mainLayout->addWidget(buttonBox);
0431 
0432         m_configureDialog->setWindowTitle( settingsWidget->windowTitle() );  // setupUi() sets this
0433         if( m_itdb )
0434         {
0435             // we will never initialize this iPod this time, hide ui for it completely
0436             m_configureDialogUi.modelComboLabel->hide();
0437             m_configureDialogUi.modelComboBox->hide();
0438             m_configureDialogUi.initializeLabel->hide();
0439             m_configureDialogUi.initializeButton->hide();
0440         }
0441 
0442         connect( m_configureDialogUi.initializeButton, &QPushButton::clicked, this, &IpodCollection::slotInitialize );
0443         connect( m_configureDialog, &QDialog::accepted, this, &IpodCollection::slotApplyConfiguration );
0444     }
0445     QScopedPointer<Capabilities::TranscodeCapability> tc( create<Capabilities::TranscodeCapability>() );
0446     IpodDeviceHelper::fillInConfigureDialog( m_configureDialog, &m_configureDialogUi,
0447                                              m_mountPoint, m_itdb, tc->savedConfiguration(),
0448                                              errorMessage );
0449 
0450     // don't allow to resize the dialog too small:
0451     m_configureDialog->setMinimumSize( m_configureDialog->sizeHint() );
0452     m_configureDialog->show();
0453     m_configureDialog->raise();
0454 }
0455 
0456 void IpodCollection::collectionUpdated()
0457 {
0458     m_lastUpdated = QDateTime::currentMSecsSinceEpoch();
0459     Q_EMIT updated();
0460 }
0461 
0462 void
0463 IpodCollection::slotInitialize()
0464 {
0465     if( m_itdb )
0466         return;  // why the hell we were called?
0467 
0468     m_configureDialogUi.initializeButton->setEnabled( false );
0469     QString errorMessage;
0470     bool success = IpodDeviceHelper::initializeIpod( m_mountPoint, &m_configureDialogUi, errorMessage );
0471     if( !success )
0472     {
0473         slotShowConfigureDialogWithError( errorMessage );
0474         return;
0475     }
0476 
0477     errorMessage.clear();
0478     m_itdb = IpodDeviceHelper::parseItdb( m_mountPoint, errorMessage );
0479     m_prettyName = IpodDeviceHelper::collectionName( m_itdb ); // allows null m_itdb
0480 
0481     if( m_itdb )
0482     {
0483         QScopedPointer<Capabilities::TranscodeCapability> tc( create<Capabilities::TranscodeCapability>() );
0484         errorMessage = i18nc( "iPod was successfully initialized", "Initialization successful." );
0485         // so that the buttons are re-enabled, info filled etc:
0486         IpodDeviceHelper::fillInConfigureDialog( m_configureDialog, &m_configureDialogUi,
0487             m_mountPoint, m_itdb, tc->savedConfiguration(), errorMessage );
0488 
0489         // there will be probably 0 tracks, but it may do more in future, for example stale
0490         // & orphaned track search.
0491         IpodParseTracksJob *job = new IpodParseTracksJob( this );
0492         connect( job, &IpodParseTracksJob::done, job, &QObject::deleteLater );
0493         ThreadWeaver::Queue::instance()->enqueue( QSharedPointer<ThreadWeaver::Job>(job) );
0494     }
0495     else
0496         slotShowConfigureDialogWithError( errorMessage ); // shows error message and allows initializing
0497 }
0498 
0499 void
0500 IpodCollection::slotApplyConfiguration()
0501 {
0502     if( !isWritable() )
0503         return; // we can do nothing if we are not writeable
0504 
0505     QString newName = m_configureDialogUi.nameLineEdit->text();
0506     if( !newName.isEmpty() && newName != IpodDeviceHelper::ipodName( m_itdb ) )
0507     {
0508         IpodDeviceHelper::setIpodName( m_itdb, newName );
0509         m_prettyName = IpodDeviceHelper::collectionName( m_itdb );
0510         Q_EMIT startWriteDatabaseTimer(); // the change should be written down to the database
0511         Q_EMIT startUpdateTimer();
0512     }
0513 
0514     QScopedPointer<Capabilities::TranscodeCapability> tc( create<Capabilities::TranscodeCapability>() );
0515     tc->setSavedConfiguration( m_configureDialogUi.transcodeComboBox->currentChoice() );
0516 }
0517 
0518 void
0519 IpodCollection::slotStartUpdateTimer()
0520 {
0521     // there are no concurrency problems, this method can only be called from the main
0522     // thread and that's where the timer fires
0523     if( m_updateTimer.isActive() )
0524         return; // already running, nothing to do
0525 
0526     // number of milliseconds to next desired update, may be negative
0527     int timeout = m_lastUpdated + 1000 - QDateTime::currentMSecsSinceEpoch();
0528     // give at least 50 msecs to catch multi-tracks edits nicely on the first frame
0529     m_updateTimer.start( qBound( 50, timeout, 1000 ) );
0530 }
0531 
0532 void
0533 IpodCollection::slotStartWriteDatabaseTimer()
0534 {
0535     m_writeDatabaseTimer.start( 30000 );
0536     // ensure we have a file on iPod open that prevents unmounting it if db is dirty
0537     if( !m_preventUnmountTempFile )
0538     {
0539         m_preventUnmountTempFile = new QTemporaryFile();
0540         QString name( "/.itunes_database_dirty_in_amarok_prevent_unmounting" );
0541         m_preventUnmountTempFile->setFileTemplate( m_mountPoint + name );
0542         m_preventUnmountTempFile->open();
0543     }
0544 }
0545 
0546 void IpodCollection::slotInitiateDatabaseWrite()
0547 {
0548     if( m_writeDatabaseJob )
0549     {
0550         warning() << __PRETTY_FUNCTION__ << "called while m_writeDatabaseJob still points"
0551                   << "to an older job. Not doing anything.";
0552         return;
0553     }
0554     IpodWriteDatabaseJob *job = new IpodWriteDatabaseJob( this );
0555     m_writeDatabaseJob = job;
0556     connect( job, &IpodWriteDatabaseJob::done, job, &QObject::deleteLater );
0557     ThreadWeaver::Queue::instance()->enqueue( QSharedPointer<ThreadWeaver::Job>(job) );
0558 }
0559 
0560 void IpodCollection::slotPerformTeardownAndRemove()
0561 {
0562     /* try to eject the device from system. Following technique potentially catches more
0563      * cases than simply passing the udi from IpodCollectionFactory, think of fuse-based
0564      * filesystems for mounting iPhones et cetera.. */
0565     Solid::Predicate query( Solid::DeviceInterface::StorageAccess, QString( "filePath" ),
0566                             m_mountPoint );
0567     QList<Solid::Device> devices = Solid::Device::listFromQuery( query );
0568     if( devices.count() == 1 )
0569     {
0570         Solid::Device device = devices.at( 0 );
0571         Solid::StorageAccess *ssa = device.as<Solid::StorageAccess>();
0572         if( ssa )
0573             ssa->teardown();
0574     }
0575 
0576     slotRemove();
0577 }
0578 
0579 void IpodCollection::slotRemove()
0580 {
0581     // this is not racy, we are in the main thread and parseTracksJob can be deleted only
0582     // in the main thread
0583     if( m_parseTracksJob )
0584     {
0585         // we need to wait until parseTracksJob finishes, because it accesses IpodCollection
0586         // and IpodPlaylistProvider in an asynchronous way that cannot safely cope with
0587         // IpodCollection disappearing
0588         connect( m_parseTracksJob.data(), &QObject::destroyed, this, &IpodCollection::remove );
0589         m_parseTracksJob->abort();
0590     }
0591     else
0592         Q_EMIT remove();
0593 }
0594 
0595 Meta::TrackPtr
0596 IpodCollection::addTrack( IpodMeta::Track *track )
0597 {
0598     if( !track || !m_itdb )
0599         return Meta::TrackPtr();
0600 
0601     Itdb_Track *itdbTrack = track->itdbTrack();
0602     bool justAdded = false;
0603 
0604     m_itdbMutex.lock();
0605     Q_ASSERT( !itdbTrack->itdb || itdbTrack->itdb == m_itdb /* refuse to take track from another itdb */ );
0606     if( !itdbTrack->itdb )
0607     {
0608         itdb_track_add( m_itdb, itdbTrack, -1 );
0609         // if it wasn't in itdb, it couldn't have legally been in master playlist
0610         // TODO: podcasts should not go into MPL
0611         itdb_playlist_add_track( itdb_playlist_mpl( m_itdb ), itdbTrack, -1 );
0612 
0613         justAdded = true;
0614         Q_EMIT startWriteDatabaseTimer();
0615     }
0616     track->setCollection( QPointer<IpodCollection>( this ) );
0617 
0618     Meta::TrackPtr trackPtr( track );
0619     Meta::TrackPtr memTrack = MemoryMeta::MapChanger( m_mc.data() ).addTrack( trackPtr );
0620     if( !memTrack && justAdded )
0621     {
0622         /* this new track was not added to MemoryCollection, it may vanish soon, prevent
0623          * dangling pointer in m_itdb */
0624         itdb_playlist_remove_track( nullptr /* = MPL */, itdbTrack );
0625         itdb_track_unlink( itdbTrack );
0626     }
0627     m_itdbMutex.unlock();
0628 
0629     if( memTrack )
0630     {
0631         subscribeTo( trackPtr );
0632         Q_EMIT startUpdateTimer();
0633     }
0634     return memTrack;
0635 }
0636 
0637 void
0638 IpodCollection::removeTrack( const Meta::TrackPtr &track )
0639 {
0640     if( !track )
0641         return; // nothing to do
0642     /* Following call ensures thread-safety even when this method is called multiple times
0643      * from different threads with the same track: only one thread will get non-null
0644      * deletedTrack from MapChanger. */
0645     Meta::TrackPtr deletedTrack = MemoryMeta::MapChanger( m_mc.data() ).removeTrack( track );
0646     if( !deletedTrack )
0647     {
0648         warning() << __PRETTY_FUNCTION__ << "attempt to delete a track that was not in"
0649                   << "MemoryCollection or not added using MapChanger";
0650         return;
0651     }
0652     IpodMeta::Track *ipodTrack = dynamic_cast<IpodMeta::Track *>( deletedTrack.data() );
0653     if( !ipodTrack )
0654     {
0655         warning() << __PRETTY_FUNCTION__ << "attempt to delete a track that was not"
0656                   << "internally iPod track";
0657         return;
0658     }
0659 
0660     Itdb_Track *itdbTrack = ipodTrack->itdbTrack();
0661     if( itdbTrack->itdb && m_itdb )
0662     {
0663         // remove from all playlists excluding the MPL:
0664         m_playlistProvider->removeTrackFromPlaylists( track );
0665 
0666         QMutexLocker locker( &m_itdbMutex );
0667         // remove track from the master playlist:
0668         itdb_playlist_remove_track( itdb_playlist_mpl( m_itdb ), itdbTrack );
0669         // remove it from the db:
0670         itdb_track_unlink( itdbTrack );
0671         Q_EMIT startWriteDatabaseTimer();
0672     }
0673 
0674     Q_EMIT startUpdateTimer();
0675 }
0676 
0677 bool IpodCollection::writeDatabase()
0678 {
0679     if( !IpodDeviceHelper::safeToWrite( m_mountPoint, m_itdb ) ) // returns false if m_itdb is null
0680     {
0681         // we have to delete unmount-preventing file even in this case
0682         delete m_preventUnmountTempFile;
0683         m_preventUnmountTempFile = nullptr;
0684         warning() << "Refusing to write iTunes database to iPod becauase device is not safe to write";
0685         return false;
0686     }
0687 
0688     m_itdbMutex.lock();
0689     GError *error = nullptr;
0690     bool success = itdb_write( m_itdb, &error );
0691     m_itdbMutex.unlock();
0692     QString gpodError;
0693     if( error )
0694     {
0695         gpodError = QString::fromUtf8( error->message );
0696         g_error_free( error );
0697         error = nullptr;
0698     }
0699     delete m_preventUnmountTempFile;  // this deletes the file
0700     m_preventUnmountTempFile = nullptr;
0701 
0702     if( success )
0703     {
0704         QString message = i18nc( "%1: iPod collection name",
0705                          "iTunes database successfully written to %1", prettyName() );
0706         Amarok::Logger::shortMessage( message );
0707     }
0708     else
0709     {
0710         QString message;
0711         if( gpodError.isEmpty() )
0712             message = i18nc( "%1: iPod collection name",
0713                              "Writing iTunes database to %1 failed without an indication of error",
0714                              prettyName() );
0715         else
0716             message = i18nc( "%1: iPod collection name, %2: technical error from libgpod",
0717                              "Writing iTunes database to %1 failed: %2", prettyName(), gpodError );
0718         Amarok::Logger::longMessage( message );
0719     }
0720     return success;
0721 }
0722