File indexing completed on 2025-01-05 04:26:01
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 "IpodPlaylistProvider.h" 0018 0019 #include "IpodCollection.h" 0020 #include "IpodCollectionLocation.h" 0021 #include "IpodPlaylist.h" 0022 #include "core/capabilities/ActionsCapability.h" 0023 #include "core/logger/Logger.h" 0024 #include "core/support/Components.h" 0025 #include "core/support/Debug.h" 0026 #include "core-impl/collections/support/FileCollectionLocation.h" 0027 0028 #include <QDir> 0029 #include <QFileInfo> 0030 0031 #include <gpod/itdb.h> 0032 0033 IpodPlaylistProvider::IpodPlaylistProvider( IpodCollection* collection ) 0034 : UserPlaylistProvider( collection ) 0035 , m_coll( collection ) 0036 { 0037 } 0038 0039 IpodPlaylistProvider::~IpodPlaylistProvider() 0040 { 0041 } 0042 0043 QString 0044 IpodPlaylistProvider::prettyName() const 0045 { 0046 return m_coll->prettyName(); 0047 } 0048 0049 QIcon 0050 IpodPlaylistProvider::icon() const 0051 { 0052 return m_coll->icon(); 0053 } 0054 0055 int 0056 IpodPlaylistProvider::playlistCount() const 0057 { 0058 return m_playlists.count(); 0059 } 0060 0061 Playlists::PlaylistList 0062 IpodPlaylistProvider::playlists() 0063 { 0064 return m_playlists; 0065 } 0066 0067 Playlists::PlaylistPtr 0068 IpodPlaylistProvider::addPlaylist(Playlists::PlaylistPtr playlist ) 0069 { 0070 return save( playlist->tracks(), playlist->name() ); 0071 } 0072 0073 Meta::TrackPtr 0074 IpodPlaylistProvider::addTrack(const Meta::TrackPtr &track ) 0075 { 0076 QString name = QLocale().toString( QDateTime::currentDateTime() ); 0077 return save( Meta::TrackList() << track , name )->tracks().last(); 0078 } 0079 0080 Playlists::PlaylistPtr 0081 IpodPlaylistProvider::save( const Meta::TrackList &tracks, const QString &name ) 0082 { 0083 if( !isWritable() ) 0084 return Playlists::PlaylistPtr(); 0085 0086 IpodPlaylist *playlist = new IpodPlaylist( tracks, name, m_coll ); 0087 itdb_playlist_add( m_coll->m_itdb, playlist->itdbPlaylist(), -1 ); 0088 Playlists::PlaylistPtr playlistPtr( playlist ); 0089 m_playlists << playlistPtr; 0090 subscribeTo( playlistPtr ); 0091 Q_EMIT playlistAdded( playlistPtr ); 0092 Q_EMIT startWriteDatabaseTimer(); 0093 return playlistPtr; 0094 } 0095 0096 QActionList 0097 IpodPlaylistProvider::providerActions() 0098 { 0099 QScopedPointer<Capabilities::ActionsCapability> ac( m_coll->create<Capabilities::ActionsCapability>() ); 0100 return ac->actions(); // all IpodCollection actions are parented, no memory leak here 0101 } 0102 0103 QActionList 0104 IpodPlaylistProvider::playlistActions( const Playlists::PlaylistList &playlists ) 0105 { 0106 QActionList actions; 0107 foreach( const Playlists::PlaylistPtr &playlist, playlists ) 0108 { 0109 if( !m_playlists.contains( playlist ) ) // make following static cast safe 0110 continue; 0111 IpodPlaylist::Type type = AmarokSharedPointer<IpodPlaylist>::staticCast( playlist )->type(); 0112 if( type == IpodPlaylist::Stale || type == IpodPlaylist::Orphaned ) 0113 { 0114 actions << m_coll->m_consolidateAction; 0115 break; 0116 } 0117 } 0118 0119 return actions; 0120 } 0121 0122 QActionList 0123 IpodPlaylistProvider::trackActions( const QMultiHash<Playlists::PlaylistPtr, int> &playlistTracks ) 0124 { 0125 return playlistActions( playlistTracks.uniqueKeys() ); 0126 } 0127 0128 bool 0129 IpodPlaylistProvider::isWritable() 0130 { 0131 return m_coll->isWritable(); 0132 } 0133 0134 void 0135 IpodPlaylistProvider::renamePlaylist( Playlists::PlaylistPtr playlist, const QString &newName ) 0136 { 0137 if( !m_playlists.contains( playlist ) ) // make following static cast safe 0138 return; 0139 AmarokSharedPointer<IpodPlaylist> ipodPlaylist = AmarokSharedPointer<IpodPlaylist>::staticCast( playlist ); 0140 if( ipodPlaylist->type() != IpodPlaylist::Normal ) 0141 return; // special playlists cannot be renamed 0142 0143 playlist->setName( newName ); 0144 Q_EMIT updated(); 0145 Q_EMIT startWriteDatabaseTimer(); 0146 } 0147 0148 bool 0149 IpodPlaylistProvider::deletePlaylists( const Playlists::PlaylistList &playlistlist ) 0150 { 0151 if( !isWritable() ) 0152 return false; 0153 0154 foreach( Playlists::PlaylistPtr playlist, playlistlist ) 0155 { 0156 if( !m_playlists.contains( playlist ) ) 0157 continue; 0158 if( AmarokSharedPointer<IpodPlaylist>::staticCast( playlist )->type() != IpodPlaylist::Normal ) 0159 continue; // special playlists cannot be removed using this method 0160 m_playlists.removeOne( playlist ); 0161 0162 unsubscribeFrom( playlist ); 0163 IpodPlaylist *ipodPlaylist = static_cast<IpodPlaylist *>( playlist.data() ); 0164 itdb_playlist_unlink( ipodPlaylist->itdbPlaylist() ); 0165 0166 Q_EMIT playlistRemoved( playlist ); 0167 Q_EMIT startWriteDatabaseTimer(); 0168 } 0169 return true; 0170 } 0171 0172 void 0173 IpodPlaylistProvider::metadataChanged(const Playlists::PlaylistPtr & ) 0174 { 0175 Q_EMIT startWriteDatabaseTimer(); 0176 } 0177 0178 void 0179 IpodPlaylistProvider::trackAdded(const Playlists::PlaylistPtr &, const Meta::TrackPtr &, int ) 0180 { 0181 Q_EMIT startWriteDatabaseTimer(); 0182 } 0183 0184 void 0185 IpodPlaylistProvider::trackRemoved(const Playlists::PlaylistPtr &, int ) 0186 { 0187 Q_EMIT startWriteDatabaseTimer(); 0188 } 0189 0190 void 0191 IpodPlaylistProvider::scheduleCopyAndInsertToPlaylist( AmarokSharedPointer<IpodPlaylist> playlist ) 0192 { 0193 m_copyTracksTo.insert( playlist ); 0194 QTimer::singleShot( 0, this, &IpodPlaylistProvider::slotCopyAndInsertToPlaylists ); 0195 } 0196 0197 void 0198 IpodPlaylistProvider::removeTrackFromPlaylists( Meta::TrackPtr track ) 0199 { 0200 foreach( Playlists::PlaylistPtr playlist, m_playlists ) 0201 { 0202 int trackIndex; 0203 // track may be multiple times in a playlist: 0204 while( ( trackIndex = playlist->tracks().indexOf( track ) ) >= 0 ) 0205 playlist->removeTrack( trackIndex ); 0206 } 0207 } 0208 0209 bool 0210 IpodPlaylistProvider::hasStaleOrOrphaned() const 0211 { 0212 return m_stalePlaylist || m_orphanedPlaylist; 0213 } 0214 0215 void 0216 IpodPlaylistProvider::slotConsolidateStaleOrphaned() 0217 { 0218 int matched = 0, added = 0, removed = 0, failed = 0; 0219 0220 /* Sometimes users accidentally rename files on iPod. This creates a pair of a stale 0221 * iTunes database entry and an orphaned file. Find these specifically and move the files 0222 * back to their original location. */ 0223 if( m_stalePlaylist && m_orphanedPlaylist ) 0224 { 0225 QMap<Meta::TrackPtr, Meta::TrackPtr> orphanedToStale; 0226 foreach( Meta::TrackPtr orphanedTrack, m_orphanedPlaylist->tracks() ) 0227 { 0228 Meta::TrackPtr matchingStaleTrack; 0229 foreach( Meta::TrackPtr staleTrack, m_stalePlaylist->tracks() ) 0230 { 0231 if( orphanedAndStaleTracksMatch( orphanedTrack, staleTrack ) ) 0232 { 0233 matchingStaleTrack = staleTrack; 0234 break; 0235 } 0236 } 0237 0238 if( matchingStaleTrack ) // matching track found 0239 { 0240 orphanedToStale.insert( orphanedTrack, matchingStaleTrack ); 0241 m_stalePlaylist->removeTrack( m_stalePlaylist->tracks().indexOf( matchingStaleTrack ) ); 0242 } 0243 } 0244 0245 QMapIterator<Meta::TrackPtr, Meta::TrackPtr> it( orphanedToStale ); 0246 while( it.hasNext() ) 0247 { 0248 it.next(); 0249 Meta::TrackPtr orphanedTrack = it.key(); 0250 Meta::TrackPtr staleTrack = it.value(); 0251 m_orphanedPlaylist->removeTrack( m_orphanedPlaylist->tracks().indexOf( orphanedTrack ) ); 0252 0253 QString from = orphanedTrack->playableUrl().toLocalFile(); 0254 QString to = staleTrack->playableUrl().toLocalFile(); 0255 if( !QFileInfo( to ).absoluteDir().mkpath( "." ) ) 0256 { 0257 warning() << __PRETTY_FUNCTION__ << "Failed to create directory path" 0258 << QFileInfo( to ).absoluteDir().path(); 0259 failed++; 0260 continue; 0261 } 0262 if( !QFile::rename( from, to ) ) 0263 { 0264 warning() << __PRETTY_FUNCTION__ << "Failed to move track from" << from 0265 << "to" << to; 0266 failed++; 0267 continue; 0268 } 0269 matched++; 0270 } 0271 } 0272 0273 // remove remaining stale tracks 0274 if( m_stalePlaylist && m_stalePlaylist->trackCount() ) 0275 { 0276 Collections::CollectionLocation *location = m_coll->location(); 0277 // hide removal confirmation - these tracks are already deleted, don't confuse user 0278 static_cast<IpodCollectionLocation *>( location )->setHidingRemoveConfirm( true ); 0279 removed += m_stalePlaylist->trackCount(); 0280 location->prepareRemove( m_stalePlaylist->tracks() ); 0281 // remove all tracks from the playlist, assume the removal succeeded 0282 while( m_stalePlaylist->trackCount() ) 0283 m_stalePlaylist->removeTrack( 0 ); 0284 } 0285 0286 // add remaining orphaned tracks back to database 0287 if( m_orphanedPlaylist && m_orphanedPlaylist->trackCount() ) 0288 { 0289 Collections::CollectionLocation *src = new Collections::FileCollectionLocation(); 0290 Collections::CollectionLocation *dest = m_coll->location(); 0291 added += m_orphanedPlaylist->trackCount(); 0292 src->prepareMove( m_orphanedPlaylist->tracks(), dest ); 0293 // remove all tracks from the playlist, assume the move succeeded 0294 while( m_orphanedPlaylist->trackCount() ) 0295 m_orphanedPlaylist->removeTrack( 0 ); 0296 } 0297 0298 // if some of the playlists became empty, remove them completely. no need to 0299 // unsubscribe - we were not subscribed 0300 if( m_stalePlaylist && m_stalePlaylist->trackCount() == 0 ) 0301 { 0302 m_playlists.removeOne( m_stalePlaylist ); 0303 Q_EMIT playlistRemoved( m_stalePlaylist ); 0304 m_stalePlaylist = nullptr; 0305 } 0306 if( m_orphanedPlaylist && m_orphanedPlaylist->trackCount() == 0 ) 0307 { 0308 m_playlists.removeOne( m_orphanedPlaylist ); 0309 Q_EMIT playlistRemoved( m_orphanedPlaylist ); 0310 m_orphanedPlaylist = nullptr; 0311 } 0312 0313 QString failedText = failed ? i18np("Failed to process one track. (more info about " 0314 "it is in the Amarok debugging log)", "Failed to process %1 tracks. (more info " 0315 "about these is in the Amarok debugging log)", failed ) : QString(); 0316 QString text = i18nc( "Infrequently displayed message, don't bother with singular " 0317 "forms. %1 to %3 are numbers, %4 is the 'Failed to process ...' sentence or an " 0318 "empty string.", "Done consolidating iPod files. %1 orphaned tracks matched with " 0319 "stale iTunes database entries, %2 stale database entries removed, %3 orphaned " 0320 "tracks added back to the iTunes database. %4", matched, removed, added, 0321 failedText ); 0322 Amarok::Logger::longMessage( text ); 0323 } 0324 0325 void 0326 IpodPlaylistProvider::slotCopyAndInsertToPlaylists() 0327 { 0328 QMutableSetIterator< AmarokSharedPointer<IpodPlaylist> > it( m_copyTracksTo ); 0329 while( it.hasNext() ) 0330 { 0331 AmarokSharedPointer<IpodPlaylist> ipodPlaylist = it.next(); 0332 TrackPositionList tracks = ipodPlaylist->takeTracksToCopy(); 0333 copyAndInsertToPlaylist( tracks, Playlists::PlaylistPtr::staticCast( ipodPlaylist ) ); 0334 it.remove(); 0335 } 0336 } 0337 0338 void IpodPlaylistProvider::copyAndInsertToPlaylist( const TrackPositionList &tracks, Playlists::PlaylistPtr destPlaylist ) 0339 { 0340 QMap<Collections::Collection*, TrackPositionList> sourceCollections; 0341 foreach( const TrackPosition &pair, tracks ) 0342 { 0343 Collections::Collection *coll = pair.first->collection(); 0344 if( coll == m_coll ) 0345 continue; 0346 0347 if( sourceCollections.contains( coll ) ) 0348 sourceCollections[ coll ] << pair; 0349 else 0350 sourceCollections.insert( coll, TrackPositionList() << pair ); 0351 } 0352 0353 foreach( Collections::Collection *coll, sourceCollections.keys() ) 0354 { 0355 Meta::TrackList sourceTracks; 0356 QMap<Meta::TrackPtr, int> trackPlaylistPositions; 0357 foreach( const TrackPosition &pair, sourceCollections.value( coll ) ) 0358 { 0359 sourceTracks << pair.first; 0360 trackPlaylistPositions.insert( pair.first, pair.second ); 0361 } 0362 0363 Collections::CollectionLocation *sourceLocation = coll 0364 ? coll->location() : new Collections::FileCollectionLocation(); 0365 Q_ASSERT( sourceLocation ); 0366 // prepareCopy() takes ownership of the pointers, we must create target collection every time 0367 IpodCollectionLocation *targetLocation = static_cast<IpodCollectionLocation *>( m_coll->location() ); 0368 0369 targetLocation->setDestinationPlaylist( destPlaylist, trackPlaylistPositions ); 0370 sourceLocation->prepareCopy( sourceTracks, targetLocation ); 0371 } 0372 } 0373 0374 bool 0375 IpodPlaylistProvider::orphanedAndStaleTracksMatch( const Meta::TrackPtr &orphaned, const Meta::TrackPtr &stale ) 0376 { 0377 if( orphaned->filesize() != stale->filesize() ) // first for performance reasons 0378 return false; 0379 if( orphaned->length() != stale->length() ) 0380 return false; 0381 if( orphaned->name() != stale->name() ) 0382 return false; 0383 if( orphaned->type() != stale->type() ) 0384 return false; 0385 if( orphaned->trackNumber() != stale->trackNumber() ) 0386 return false; 0387 if( orphaned->discNumber() != stale->discNumber() ) 0388 return false; 0389 0390 if( entitiesDiffer( orphaned->album(), stale->album() ) ) 0391 return false; 0392 if( entitiesDiffer( orphaned->artist(), stale->artist() ) ) 0393 return false; 0394 if( entitiesDiffer( orphaned->composer(), stale->composer() ) ) 0395 return false; 0396 if( entitiesDiffer( orphaned->genre(), stale->genre() ) ) 0397 return false; 0398 if( entitiesDiffer( orphaned->year(), stale->year() ) ) 0399 return false; 0400 0401 return true; 0402 } 0403 0404 template <class T> bool 0405 IpodPlaylistProvider::entitiesDiffer( T first, T second ) 0406 { 0407 return ( first ? first->name() : QString() ) != ( second ? second->name() : QString() ); 0408 } 0409