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 }