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