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