File indexing completed on 2024-05-05 04:48:42

0001 /****************************************************************************************
0002  * Copyright (c) 2007-2008 Ian Monroe <ian@monroe.nu>                                   *
0003  * Copyright (c) 2007-2008 Nikolaj Hald Nielsen <nhn@kde.org>                           *
0004  * Copyright (c) 2008 Seb Ruiz <ruiz@kde.org>                                           *
0005  * Copyright (c) 2008 Soren Harward <stharward@gmail.com>                               *
0006  * Copyright (c) 2009 John Atkinson <john@fauxnetic.co.uk>                              *
0007  * Copyright (c) 2009,2010 Téo Mrnjavac <teo@kde.org>                                   *
0008  *                                                                                      *
0009  * This program is free software; you can redistribute it and/or modify it under        *
0010  * the terms of the GNU General Public License as published by the Free Software        *
0011  * Foundation; either version 2 of the License, or (at your option) version 3 or        *
0012  * any later version accepted by the membership of KDE e.V. (or its successor approved  *
0013  * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of  *
0014  * version 3 of the license.                                                            *
0015  *                                                                                      *
0016  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0017  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0018  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0019  *                                                                                      *
0020  * You should have received a copy of the GNU General Public License along with         *
0021  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0022  ****************************************************************************************/
0023 
0024 #define DEBUG_PREFIX "Playlist::Controller"
0025 
0026 // WORKAROUND for QTBUG-25960. Required for Qt versions < 4.8.5 in combination with libc++.
0027 #define QT_NO_STL 1
0028     #include <qiterator.h>
0029 #undef QT_NO_STL
0030 
0031 #include "PlaylistController.h"
0032 
0033 #include "EngineController.h"
0034 #include "amarokconfig.h"
0035 #include "core/collections/QueryMaker.h"
0036 #include "core/support/Debug.h"
0037 #include "core-impl/meta/cue/CueFileSupport.h"
0038 #include "core-impl/meta/file/File.h"
0039 #include "core-impl/meta/multi/MultiTrack.h"
0040 #include "core-impl/playlists/types/file/PlaylistFileSupport.h"
0041 #include "core-impl/support/TrackLoader.h"
0042 #include "playlist/PlaylistActions.h"
0043 #include "playlist/PlaylistModelStack.h"
0044 #include "playlistmanager/PlaylistManager.h"
0045 
0046 #include <QAction>
0047 
0048 #include <algorithm>
0049 #include <typeinfo>
0050 
0051 using namespace Playlist;
0052 
0053 namespace The
0054 {
0055     AMAROK_EXPORT Controller* playlistController()
0056     {
0057         return Controller::instance();
0058     }
0059 }
0060 
0061 
0062 Controller* Controller::s_instance = nullptr;
0063 
0064 Controller*
0065 Controller::instance()
0066 {
0067     if( s_instance == nullptr )
0068         s_instance = new Controller();
0069     return s_instance;
0070 }
0071 
0072 void
0073 Controller::destroy()
0074 {
0075     if( s_instance )
0076     {
0077         delete s_instance;
0078         s_instance = nullptr;
0079     }
0080 }
0081 
0082 Controller::Controller()
0083         : QObject()
0084         , m_undoStack( new QUndoStack( this ) )
0085 {
0086     DEBUG_BLOCK
0087 
0088     //As a rule, when talking to the playlist one should always use the topmost model, as
0089     //Playlist::ModelStack::instance->top() or simply The::playlist().
0090     //This is an exception, because we handle the presence of tracks in the bottom model,
0091     //so we get a pointer to the bottom model and use it with great care.
0092     // TODO: get these values only when we really need them to loosen up the
0093     // coupling between Controller and Model
0094     m_bottomModel = ModelStack::instance()->bottom();
0095     m_topModel = The::playlist();
0096 
0097     m_undoStack->setUndoLimit( 20 );
0098     connect( m_undoStack, &QUndoStack::canRedoChanged, this, &Controller::canRedoChanged );
0099     connect( m_undoStack, &QUndoStack::canUndoChanged, this, &Controller::canUndoChanged );
0100 }
0101 
0102 Controller::~Controller() {}
0103 
0104 void
0105 Controller::insertOptioned( Meta::TrackPtr track, AddOptions options )
0106 {
0107     if( !track )
0108         return;
0109 
0110     Meta::TrackList list;
0111     list.append( track );
0112     insertOptioned( list, options );
0113 }
0114 
0115 void
0116 Controller::insertOptioned( Meta::TrackList list, AddOptions options )
0117 {
0118     DEBUG_BLOCK
0119     /* Note: don't use (options & flag) here to test whether flag is present in options.
0120      * We have compound flags and for example (Queue & DirectPlay) == Queue, which
0121      * evaluates to true, which isn't usually what you want.
0122      *
0123      * Use (options & flag == flag) instead, or rather QFlag's convenience method:
0124      * options.testFlag( flag )
0125      */
0126 
0127     if( list.isEmpty() )
0128         return;
0129 
0130     QString actionName = i18nc( "name of the action in undo stack", "Add tracks to playlist" );
0131     if( options.testFlag( Queue ) )
0132         actionName = i18nc( "name of the action in undo stack", "Queue tracks" );
0133     if( options.testFlag( Replace ) )
0134         actionName = i18nc( "name of the action in undo stack", "Replace playlist" );
0135     m_undoStack->beginMacro( actionName );
0136 
0137     if( options.testFlag( Replace ) )
0138     {
0139         Q_EMIT replacingPlaylist();   //make sure that we clear filters
0140         clear();
0141         //make sure that we turn off dynamic mode.
0142         Amarok::actionCollection()->action( QStringLiteral("disable_dynamic") )->trigger();
0143     }
0144 
0145     int bottomModelRowCount = m_bottomModel->qaim()->rowCount();
0146     int bottomModelInsertRow;
0147     if( options.testFlag( Queue ) )
0148     {
0149         // queue is a list of playlist item ids
0150         QQueue<quint64> queue = Actions::instance()->queue();
0151         int activeRow = m_bottomModel->activeRow();
0152 
0153         if( options.testFlag( PrependToQueue ) )
0154         {
0155             if( activeRow >= 0 )
0156                 bottomModelInsertRow = activeRow + 1; // right after active track
0157             else if( !queue.isEmpty() )
0158                 bottomModelInsertRow = m_bottomModel->rowForId( queue.first() ); // prepend to queue
0159             else
0160                 bottomModelInsertRow = bottomModelRowCount; // fallback: append to end
0161         }
0162         else // append to queue
0163         {
0164             if( !queue.isEmpty() )
0165                 bottomModelInsertRow = m_bottomModel->rowForId( queue.last() ) + 1; // after queue
0166             else if( activeRow >= 0 )
0167                 bottomModelInsertRow = activeRow + 1; // after active track
0168             else
0169                 bottomModelInsertRow = bottomModelRowCount; // fallback: append to end
0170         }
0171     }
0172     else
0173         bottomModelInsertRow = bottomModelRowCount;
0174 
0175     // this guy does the thing:
0176     insertionHelper( bottomModelInsertRow, list );
0177 
0178     if( options.testFlag( Queue ) )
0179     {
0180         // Construct list of rows to be queued
0181         QList<quint64> ids;
0182         for( int bottomModelRow = bottomModelInsertRow;
0183              bottomModelRow < bottomModelInsertRow + list.size(); bottomModelRow++ )
0184         {
0185             ids << m_bottomModel->idAt( bottomModelRow );
0186         }
0187 
0188         if( options.testFlag( PrependToQueue ) ) // PrependToQueue implies Queue
0189         {
0190             // append current queue to new queue and remove it
0191             foreach( const quint64 id, Actions::instance()->queue() )
0192             {
0193                 Actions::instance()->dequeue( id );
0194                 ids << id;
0195             }
0196         }
0197 
0198         Actions::instance()->queue( ids );
0199     }
0200 
0201     m_undoStack->endMacro();
0202 
0203     bool startPlaying = false;
0204     EngineController *engine = The::engineController();
0205     if( options.testFlag( DirectPlay ) ) // implies PrependToQueue
0206         startPlaying = true;
0207     else if( options.testFlag( Playlist::StartPlayIfConfigured )
0208              && AmarokConfig::startPlayingOnAdd() && engine && !engine->isPlaying() )
0209     {
0210         // if nothing is in the queue, queue the first item we have added so that the call
0211         // to ->requestUserNextTrack() pops it. The queueing is therefore invisible to
0212         // user. Else we start playing the queue.
0213         if( Actions::instance()->queue().isEmpty() )
0214             Actions::instance()->queue( QList<quint64>() << m_bottomModel->idAt( bottomModelInsertRow ) );
0215 
0216         startPlaying = true;
0217     }
0218 
0219     if( startPlaying )
0220         Actions::instance()->requestUserNextTrack(); // desired track will be first in queue
0221 
0222     Q_EMIT changed();
0223 }
0224 
0225 void
0226 Controller::insertOptioned( Playlists::PlaylistPtr playlist, AddOptions options )
0227 {
0228     insertOptioned( Playlists::PlaylistList() << playlist, options );
0229 }
0230 
0231 void
0232 Controller::insertOptioned( Playlists::PlaylistList list, AddOptions options )
0233 {
0234     TrackLoader::Flags flags;
0235     // if we are going to play, we need full metadata (playable tracks)
0236     if( options.testFlag( DirectPlay ) || ( options.testFlag( Playlist::StartPlayIfConfigured )
0237         && AmarokConfig::startPlayingOnAdd() ) )
0238     {
0239         flags |= TrackLoader::FullMetadataRequired;
0240     }
0241     if( options.testFlag( Playlist::RemotePlaylistsAreStreams ) )
0242         flags |= TrackLoader::RemotePlaylistsAreStreams;
0243     TrackLoader *loader = new TrackLoader( flags ); // auto-deletes itself
0244     loader->setProperty( "options", QVariant::fromValue<AddOptions>( options ) );
0245     connect( loader, &TrackLoader::finished,
0246              this, &Controller::slotLoaderWithOptionsFinished );
0247     loader->init( list );
0248 }
0249 
0250 void
0251 Controller::insertOptioned( const QUrl &url, AddOptions options )
0252 {
0253     insertOptioned( QList<QUrl>() << url, options );
0254 }
0255 
0256 void
0257 Controller::insertOptioned( QList<QUrl> &urls, AddOptions options )
0258 {
0259     TrackLoader::Flags flags;
0260     // if we are going to play, we need full metadata (playable tracks)
0261     if( options.testFlag( DirectPlay ) || ( options.testFlag( Playlist::StartPlayIfConfigured )
0262         && AmarokConfig::startPlayingOnAdd() ) )
0263     {
0264         flags |= TrackLoader::FullMetadataRequired;
0265     }
0266     if( options.testFlag( Playlist::RemotePlaylistsAreStreams ) )
0267         flags |= TrackLoader::RemotePlaylistsAreStreams;
0268     TrackLoader *loader = new TrackLoader( flags ); // auto-deletes itself
0269     loader->setProperty( "options", QVariant::fromValue<AddOptions>( options ) );
0270     connect( loader, &TrackLoader::finished,
0271              this, &Controller::slotLoaderWithOptionsFinished );
0272     loader->init( urls );
0273 }
0274 
0275 void
0276 Controller::insertTrack( int topModelRow, Meta::TrackPtr track )
0277 {
0278     if( !track )
0279         return;
0280     insertTracks( topModelRow, Meta::TrackList() << track );
0281 }
0282 
0283 void
0284 Controller::insertTracks( int topModelRow, Meta::TrackList tl )
0285 {
0286     insertionHelper( insertionTopRowToBottom( topModelRow ), tl );
0287 }
0288 
0289 void
0290 Controller::insertPlaylist( int topModelRow, Playlists::PlaylistPtr playlist )
0291 {
0292     insertPlaylists( topModelRow, Playlists::PlaylistList() << playlist );
0293 }
0294 
0295 void
0296 Controller::insertPlaylists( int topModelRow, Playlists::PlaylistList playlists )
0297 {
0298     TrackLoader *loader = new TrackLoader(); // auto-deletes itself
0299     loader->setProperty( "topModelRow", QVariant( topModelRow ) );
0300     connect( loader, &TrackLoader::finished,
0301              this, &Controller::slotLoaderWithRowFinished );
0302     loader->init( playlists );
0303 }
0304 
0305 void
0306 Controller::insertUrls( int topModelRow, QList<QUrl> &urls )
0307 {
0308     TrackLoader *loader = new TrackLoader(); // auto-deletes itself
0309     loader->setProperty( "topModelRow", QVariant( topModelRow ) );
0310     connect( loader, &TrackLoader::finished,
0311              this, &Controller::slotLoaderWithRowFinished );
0312     loader->init( urls );
0313 }
0314 
0315 void
0316 Controller::removeRow( int topModelRow )
0317 {
0318     DEBUG_BLOCK
0319     removeRows( topModelRow, 1 );
0320 }
0321 
0322 void
0323 Controller::removeRows( int topModelRow, int count )
0324 {
0325     DEBUG_BLOCK
0326     QList<int> rl;
0327     for( int i = 0; i < count; ++i )
0328         rl.append( topModelRow++ );
0329     removeRows( rl );
0330 }
0331 
0332 void
0333 Controller::removeRows( QList<int>& topModelRows )
0334 {
0335     DEBUG_BLOCK
0336     RemoveCmdList bottomModelCmds;
0337     foreach( int topModelRow, topModelRows )
0338     {
0339         if( m_topModel->rowExists( topModelRow ) )
0340         {
0341             Meta::TrackPtr track = m_topModel->trackAt( topModelRow );    // For "undo".
0342             int bottomModelRow = m_topModel->rowToBottomModel( topModelRow );
0343             bottomModelCmds.append( RemoveCmd( track, bottomModelRow ) );
0344         }
0345         else
0346             warning() << "Received command to remove non-existent row. This should NEVER happen. row=" << topModelRow;
0347     }
0348 
0349     if( bottomModelCmds.size() > 0 )
0350         m_undoStack->push( new RemoveTracksCmd( nullptr, bottomModelCmds ) );
0351 
0352     Q_EMIT changed();
0353 }
0354 
0355 void
0356 Controller::removeDeadAndDuplicates()
0357 {
0358     DEBUG_BLOCK
0359 
0360     QList<Meta::TrackPtr> uniqueTrackList = m_topModel->tracks();
0361     QSet<Meta::TrackPtr> uniqueTracks(uniqueTrackList.begin(), uniqueTrackList.end());
0362     QList<int> topModelRowsToRemove;
0363 
0364     foreach( Meta::TrackPtr unique, uniqueTracks )
0365     {
0366         QList<int> trackRows = m_topModel->allRowsForTrack( unique ).values();
0367 
0368         if( unique->playableUrl().isLocalFile() && !QFile::exists( unique->playableUrl().path() ) )
0369         {
0370             // Track is Dead
0371             // TODO: Check remote files as well
0372             topModelRowsToRemove <<  trackRows;
0373         }
0374         else if( trackRows.size() > 1 )
0375         {
0376             // Track is Duplicated
0377             // Remove all rows except the first
0378             for( QList<int>::const_iterator it = ++trackRows.constBegin(); it != trackRows.constEnd(); ++it )
0379                 topModelRowsToRemove.push_back( *it );
0380         }
0381     }
0382 
0383     if( !topModelRowsToRemove.empty() )
0384     {
0385         m_undoStack->beginMacro( QStringLiteral("Remove dead and duplicate entries") );     // TODO: Internationalize?
0386         removeRows( topModelRowsToRemove );
0387         m_undoStack->endMacro();
0388     }
0389 }
0390 
0391 void
0392 Controller::moveRow( int from, int to )
0393 {
0394     DEBUG_BLOCK
0395     if( ModelStack::instance()->sortProxy()->isSorted() )
0396         return;
0397     if( from == to )
0398         return;
0399 
0400     QList<int> source;
0401     QList<int> target;
0402     source.append( from );
0403     source.append( to );
0404 
0405     // shift all the rows in between
0406     if( from < to )
0407     {
0408         for( int i = from + 1; i <= to; i++ )
0409         {
0410             source.append( i );
0411             target.append( i - 1 );
0412         }
0413     }
0414     else
0415     {
0416         for( int i = from - 1; i >= to; i-- )
0417         {
0418             source.append( i );
0419             target.append( i + 1 );
0420         }
0421     }
0422 
0423     reorderRows( source, target );
0424 }
0425 
0426 int
0427 Controller::moveRows( QList<int>& from, int to )
0428 {
0429     DEBUG_BLOCK
0430     if( from.size() <= 0 )
0431         return to;
0432 
0433     std::sort( from.begin(), from.end() );
0434 
0435     if( ModelStack::instance()->sortProxy()->isSorted() )
0436         return from.first();
0437 
0438     to = ( to == qBound( 0, to, m_topModel->qaim()->rowCount() ) ) ? to : m_topModel->qaim()->rowCount();
0439 
0440     from.erase( std::unique( from.begin(), from.end() ), from.end() );
0441 
0442     int min = qMin( to, from.first() );
0443     int max = qMax( to, from.last() );
0444 
0445     QList<int> source;
0446     QList<int> target;
0447     for( int i = min; i <= max; i++ )
0448     {
0449         if( i >=  m_topModel->qaim()->rowCount() )
0450             break; // we are likely moving below the last element, to an index that really does not exist, and thus should not be moved up.
0451         source.append( i );
0452         target.append( i );
0453     }
0454 
0455     int originalTo = to;
0456 
0457     foreach ( int f, from )
0458     {
0459         if( f < originalTo )
0460             to--; // since we are moving an item down in the list, this item will no longer count towards the target row
0461         source.removeOne( f );
0462     }
0463 
0464 
0465     // We iterate through the items in reverse order, as this allows us to keep the target row constant
0466     // (remember that the item that was originally on the target row is pushed down)
0467     QList<int>::const_iterator f_iter = from.constEnd();
0468     while( f_iter != from.constBegin() )
0469     {
0470         --f_iter;
0471         source.insert( ( to - min ), *f_iter );
0472     }
0473 
0474     reorderRows( source, target );
0475 
0476     return to;
0477 }
0478 
0479 void
0480 Controller::reorderRows( const QList<int> &from, const QList<int> &to )
0481 {
0482     DEBUG_BLOCK
0483     if( from.size() != to.size() )
0484         return;
0485 
0486     // validity check: each item should appear exactly once in both lists
0487     {
0488         QSet<int> fromItems( from.begin(), from.end() );
0489         QSet<int> toItems( to.begin(), to.end() );
0490 
0491         if( fromItems.size() != from.size() || toItems.size() != to.size() || fromItems != toItems )
0492         {
0493             error() << "row move lists malformed:";
0494             error() << from;
0495             error() << to;
0496             return;
0497         }
0498     }
0499 
0500     MoveCmdList bottomModelCmds;
0501     for( int i = 0; i < from.size(); i++ )
0502     {
0503         debug() << "moving rows:" << from.at( i ) << "->" << to.at( i );
0504         if( ( from.at( i ) >= 0 ) && ( from.at( i ) < m_topModel->qaim()->rowCount() ) )
0505             if( from.at( i ) != to.at( i ) )
0506                 bottomModelCmds.append( MoveCmd( m_topModel->rowToBottomModel( from.at( i ) ), m_topModel->rowToBottomModel( to.at( i ) ) ) );
0507     }
0508 
0509     if( bottomModelCmds.size() > 0 )
0510         m_undoStack->push( new MoveTracksCmd( nullptr, bottomModelCmds ) );
0511 
0512     Q_EMIT changed();
0513 }
0514 
0515 void
0516 Controller::undo()
0517 {
0518     DEBUG_BLOCK
0519     m_undoStack->undo();
0520     Q_EMIT changed();
0521 }
0522 
0523 void
0524 Controller::redo()
0525 {
0526     DEBUG_BLOCK
0527     m_undoStack->redo();
0528     Q_EMIT changed();
0529 }
0530 
0531 void
0532 Controller::clear()
0533 {
0534     DEBUG_BLOCK
0535     removeRows( 0, ModelStack::instance()->bottom()->qaim()->rowCount() );
0536     Q_EMIT changed();
0537 }
0538 
0539 /**************************************************
0540  * Private Functions
0541  **************************************************/
0542 
0543 void
0544 Controller::slotLoaderWithOptionsFinished( const Meta::TrackList &tracks )
0545 {
0546     QObject *loader = sender();
0547     if( !loader )
0548     {
0549         error() << __PRETTY_FUNCTION__ << "must be connected to TrackLoader";
0550         return;
0551     }
0552     QVariant options = loader->property( "options" );
0553     if( !options.canConvert<AddOptions>() )
0554     {
0555         error() << __PRETTY_FUNCTION__ << "loader property 'options' is not valid";
0556         return;
0557     }
0558     if( !tracks.isEmpty() )
0559         insertOptioned( tracks, options.value<AddOptions>() );
0560 }
0561 
0562 void
0563 Controller::slotLoaderWithRowFinished( const Meta::TrackList &tracks )
0564 {
0565     QObject *loader = sender();
0566     if( !loader )
0567     {
0568         error() << __PRETTY_FUNCTION__ << "must be connected to TrackLoader";
0569         return;
0570     }
0571     QVariant topModelRow = loader->property( "topModelRow" );
0572     if( !topModelRow.isValid() || topModelRow.type() != QVariant::Int )
0573     {
0574         error() << __PRETTY_FUNCTION__ << "loader property 'topModelRow' is not a valid integer";
0575         return;
0576     }
0577     if( !tracks.isEmpty() )
0578         insertTracks( topModelRow.toInt(), tracks );
0579 }
0580 
0581 int
0582 Controller::insertionTopRowToBottom( int topModelRow )
0583 {
0584     if( ( topModelRow < 0 ) || ( topModelRow > m_topModel->qaim()->rowCount() ) )
0585     {
0586         error() << "Row number invalid, using bottom:" << topModelRow;
0587         topModelRow = m_topModel->qaim()->rowCount();    // Failsafe: append.
0588     }
0589 
0590     if( ModelStack::instance()->sortProxy()->isSorted() )
0591         // if the playlist is sorted there's no point in placing the added tracks at any
0592         // specific point in relation to another track, so we just append them.
0593         return m_bottomModel->qaim()->rowCount();
0594     else
0595         return m_topModel->rowToBottomModel( topModelRow );
0596 }
0597 
0598 void
0599 Controller::insertionHelper( int bottomModelRow, Meta::TrackList& tl )
0600 {
0601     //expand any tracks that are actually playlists into multisource tracks
0602     //and any tracks with an associated cue file
0603 
0604     Meta::TrackList modifiedList;
0605 
0606     QMutableListIterator<Meta::TrackPtr> i( tl );
0607     while( i.hasNext() )
0608     {
0609         i.next();
0610         Meta::TrackPtr track = i.value();
0611 
0612         if( !track )
0613         {
0614             /*ignore*/
0615         }
0616         else if( MetaFile::TrackPtr::dynamicCast( track ) )
0617         {
0618             QUrl cuesheet = MetaCue::CueFileSupport::locateCueSheet( track->playableUrl() );
0619             if( !cuesheet.isEmpty() )
0620             {
0621                 MetaCue::CueFileItemMap cueMap = MetaCue::CueFileSupport::loadCueFile( cuesheet, track );
0622                 if( !cueMap.isEmpty() )
0623                 {
0624                     Meta::TrackList cueTracks = MetaCue::CueFileSupport::generateTimeCodeTracks( track, cueMap );
0625                     if( !cueTracks.isEmpty() )
0626                         modifiedList <<  cueTracks;
0627                     else
0628                         modifiedList << track;
0629                 }
0630                 else
0631                     modifiedList << track;
0632             }
0633             else
0634                 modifiedList << track;
0635         }
0636         else
0637         {
0638            modifiedList << track;
0639         }
0640     }
0641 
0642     InsertCmdList bottomModelCmds;
0643 
0644     foreach( Meta::TrackPtr t, modifiedList )
0645         bottomModelCmds.append( InsertCmd( t, bottomModelRow++ ) );
0646 
0647     if( bottomModelCmds.size() > 0 )
0648         m_undoStack->push( new InsertTracksCmd( nullptr, bottomModelCmds ) );
0649 
0650     Q_EMIT changed();
0651 }