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-2009 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 Téo Mrnjavac <teo@kde.org> * 0007 * * 0008 * This program is free software; you can redistribute it and/or modify it under * 0009 * the terms of the GNU General Public License as published by the Free Software * 0010 * Foundation; either version 2 of the License, or (at your option) version 3 or * 0011 * any later version accepted by the membership of KDE e.V. (or its successor approved * 0012 * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of * 0013 * version 3 of the license. * 0014 * * 0015 * This program is distributed in the hope that it will be useful, but WITHOUT ANY * 0016 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * 0017 * PARTICULAR PURPOSE. See the GNU General Public License for more details. * 0018 * * 0019 * You should have received a copy of the GNU General Public License along with * 0020 * this program. If not, see <http://www.gnu.org/licenses/>. * 0021 ****************************************************************************************/ 0022 0023 #define DEBUG_PREFIX "Playlist::Actions" 0024 0025 #include "PlaylistActions.h" 0026 0027 #include "EngineController.h" 0028 #include "MainWindow.h" 0029 #include "amarokconfig.h" 0030 #include "core/support/Amarok.h" 0031 #include "core/support/Components.h" 0032 #include "core/support/Debug.h" 0033 #include "core/logger/Logger.h" 0034 #include "core-impl/collections/support/CollectionManager.h" 0035 #include "core-impl/playlists/types/file/PlaylistFileSupport.h" 0036 #include "dynamic/DynamicModel.h" 0037 #include "navigators/DynamicTrackNavigator.h" 0038 #include "navigators/RandomAlbumNavigator.h" 0039 #include "navigators/RandomTrackNavigator.h" 0040 #include "navigators/RepeatAlbumNavigator.h" 0041 #include "navigators/RepeatTrackNavigator.h" 0042 #include "navigators/StandardTrackNavigator.h" 0043 #include "navigators/FavoredRandomTrackNavigator.h" 0044 #include "playlist/PlaylistController.h" 0045 #include "playlist/PlaylistDock.h" 0046 #include "playlist/PlaylistModelStack.h" 0047 #include "playlist/PlaylistRestorer.h" 0048 #include "playlistmanager/PlaylistManager.h" 0049 0050 #include <QRandomGenerator> 0051 #include <QStandardPaths> 0052 #include <typeinfo> 0053 0054 Playlist::Actions* Playlist::Actions::s_instance = nullptr; 0055 0056 Playlist::Actions* Playlist::Actions::instance() 0057 { 0058 if( !s_instance ) 0059 { 0060 s_instance = new Actions(); 0061 s_instance->init(); // prevent infinite recursion by using the playlist actions only after setting the instance. 0062 } 0063 return s_instance; 0064 } 0065 0066 void 0067 Playlist::Actions::destroy() 0068 { 0069 delete s_instance; 0070 s_instance = nullptr; 0071 } 0072 0073 Playlist::Actions::Actions() 0074 : QObject() 0075 , m_nextTrackCandidate( 0 ) 0076 , m_stopAfterPlayingTrackId( 0 ) 0077 , m_navigator( nullptr ) 0078 , m_waitingForNextTrack( false ) 0079 { 0080 EngineController *engine = The::engineController(); 0081 0082 if( engine ) // test cases might create a playlist without having an EngineController 0083 { 0084 connect( engine, &EngineController::trackPlaying, 0085 this, &Playlist::Actions::slotTrackPlaying ); 0086 connect( engine, &EngineController::stopped, 0087 this, &Playlist::Actions::slotPlayingStopped ); 0088 } 0089 } 0090 0091 Playlist::Actions::~Actions() 0092 { 0093 delete m_navigator; 0094 } 0095 0096 void 0097 Playlist::Actions::init() 0098 { 0099 playlistModeChanged(); // sets m_navigator. 0100 restoreDefaultPlaylist(); 0101 } 0102 0103 Meta::TrackPtr 0104 Playlist::Actions::likelyNextTrack() 0105 { 0106 return The::playlist()->trackForId( m_navigator->likelyNextTrack() ); 0107 } 0108 0109 Meta::TrackPtr 0110 Playlist::Actions::likelyPrevTrack() 0111 { 0112 return The::playlist()->trackForId( m_navigator->likelyLastTrack() ); 0113 } 0114 0115 void 0116 Playlist::Actions::requestNextTrack() 0117 { 0118 DEBUG_BLOCK 0119 if ( m_nextTrackCandidate != 0 ) 0120 return; 0121 0122 m_nextTrackCandidate = m_navigator->requestNextTrack(); 0123 if( m_nextTrackCandidate == 0 ) 0124 return; 0125 0126 if( willStopAfterTrack( ModelStack::instance()->bottom()->activeId() ) ) 0127 // Tell playlist what track to play after users hits Play again: 0128 The::playlist()->setActiveId( m_nextTrackCandidate ); 0129 else 0130 play( m_nextTrackCandidate, false ); 0131 } 0132 0133 void 0134 Playlist::Actions::requestUserNextTrack() 0135 { 0136 m_nextTrackCandidate = m_navigator->requestUserNextTrack(); 0137 play( m_nextTrackCandidate ); 0138 } 0139 0140 void 0141 Playlist::Actions::requestPrevTrack() 0142 { 0143 m_nextTrackCandidate = m_navigator->requestLastTrack(); 0144 play( m_nextTrackCandidate ); 0145 } 0146 0147 void 0148 Playlist::Actions::requestTrack( quint64 id ) 0149 { 0150 m_nextTrackCandidate = id; 0151 } 0152 0153 void 0154 Playlist::Actions::stopAfterPlayingTrack( quint64 id ) 0155 { 0156 if( id == quint64( -1 ) ) 0157 id = ModelStack::instance()->bottom()->activeId(); // 0 is fine 0158 if( id != m_stopAfterPlayingTrackId ) 0159 { 0160 m_stopAfterPlayingTrackId = id; 0161 repaintPlaylist(); // to get the visual change 0162 } 0163 } 0164 0165 bool 0166 Playlist::Actions::willStopAfterTrack( const quint64 id ) const 0167 { 0168 return m_stopAfterPlayingTrackId && m_stopAfterPlayingTrackId == id; 0169 } 0170 0171 void 0172 Playlist::Actions::play() 0173 { 0174 DEBUG_BLOCK 0175 0176 if ( m_nextTrackCandidate == 0 ) 0177 { 0178 m_nextTrackCandidate = The::playlist()->activeId(); 0179 // the queue has priority, and requestNextTrack() respects the queue. 0180 // this is a bit of a hack because we "know" that all navigators will look at the queue first. 0181 if ( !m_nextTrackCandidate || !m_navigator->queue().isEmpty() ) 0182 m_nextTrackCandidate = m_navigator->requestNextTrack(); 0183 } 0184 0185 play( m_nextTrackCandidate ); 0186 } 0187 0188 void 0189 Playlist::Actions::play( const QModelIndex &index ) 0190 { 0191 DEBUG_BLOCK 0192 0193 if( index.isValid() ) 0194 { 0195 m_nextTrackCandidate = index.data( UniqueIdRole ).value<quint64>(); 0196 play( m_nextTrackCandidate ); 0197 } 0198 } 0199 0200 void 0201 Playlist::Actions::play( const int row ) 0202 { 0203 DEBUG_BLOCK 0204 0205 m_nextTrackCandidate = The::playlist()->idAt( row ); 0206 play( m_nextTrackCandidate ); 0207 } 0208 0209 void 0210 Playlist::Actions::play( const quint64 trackid, bool now ) 0211 { 0212 DEBUG_BLOCK 0213 0214 Meta::TrackPtr track = The::playlist()->trackForId( trackid ); 0215 if ( track ) 0216 { 0217 if ( now ) 0218 The::engineController()->play( track ); 0219 else 0220 The::engineController()->setNextTrack( track ); 0221 } 0222 else 0223 { 0224 warning() << "Invalid trackid" << trackid; 0225 } 0226 } 0227 0228 void 0229 Playlist::Actions::next() 0230 { 0231 DEBUG_BLOCK 0232 requestUserNextTrack(); 0233 } 0234 0235 void 0236 Playlist::Actions::back() 0237 { 0238 DEBUG_BLOCK 0239 requestPrevTrack(); 0240 } 0241 0242 0243 void 0244 Playlist::Actions::enableDynamicMode( bool enable ) 0245 { 0246 if( AmarokConfig::dynamicMode() == enable ) 0247 return; 0248 0249 AmarokConfig::setDynamicMode( enable ); 0250 // TODO: turn off other incompatible modes 0251 // TODO: should we restore the state of other modes? 0252 AmarokConfig::self()->save(); 0253 0254 Playlist::Dock *dock = The::mainWindow()->playlistDock(); 0255 Playlist::SortWidget *sorting = dock ? dock->sortWidget() : nullptr; 0256 if( sorting ) 0257 sorting->trimToLevel(); 0258 0259 playlistModeChanged(); 0260 0261 /* append upcoming tracks to satisfy user's with about number of upcoming tracks. 0262 * Needs to be _after_ playlistModeChanged() because before calling it the old 0263 * m_navigator still reigns. */ 0264 if( enable ) 0265 normalizeDynamicPlaylist(); 0266 } 0267 0268 0269 void 0270 Playlist::Actions::playlistModeChanged() 0271 { 0272 DEBUG_BLOCK 0273 0274 QQueue<quint64> currentQueue; 0275 0276 if ( m_navigator ) 0277 { 0278 //HACK: Migrate the queue to the new navigator 0279 //TODO: The queue really should not be maintained by the navigators in this way 0280 // but should be handled by a separate and persistent object. 0281 0282 currentQueue = m_navigator->queue(); 0283 m_navigator->deleteLater(); 0284 } 0285 0286 debug() << "Dynamic mode: " << AmarokConfig::dynamicMode(); 0287 0288 if ( AmarokConfig::dynamicMode() ) 0289 { 0290 m_navigator = new DynamicTrackNavigator(); 0291 Q_EMIT navigatorChanged(); 0292 return; 0293 } 0294 0295 m_navigator = nullptr; 0296 0297 switch( AmarokConfig::trackProgression() ) 0298 { 0299 0300 case AmarokConfig::EnumTrackProgression::RepeatTrack: 0301 m_navigator = new RepeatTrackNavigator(); 0302 break; 0303 0304 case AmarokConfig::EnumTrackProgression::RepeatAlbum: 0305 m_navigator = new RepeatAlbumNavigator(); 0306 break; 0307 0308 case AmarokConfig::EnumTrackProgression::RandomTrack: 0309 switch( AmarokConfig::favorTracks() ) 0310 { 0311 case AmarokConfig::EnumFavorTracks::HigherScores: 0312 case AmarokConfig::EnumFavorTracks::HigherRatings: 0313 case AmarokConfig::EnumFavorTracks::LessRecentlyPlayed: 0314 m_navigator = new FavoredRandomTrackNavigator(); 0315 break; 0316 0317 case AmarokConfig::EnumFavorTracks::Off: 0318 default: 0319 m_navigator = new RandomTrackNavigator(); 0320 break; 0321 } 0322 break; 0323 0324 case AmarokConfig::EnumTrackProgression::RandomAlbum: 0325 m_navigator = new RandomAlbumNavigator(); 0326 break; 0327 0328 //repeat playlist, standard, only queue and fallback are all the normal navigator. 0329 case AmarokConfig::EnumTrackProgression::RepeatPlaylist: 0330 case AmarokConfig::EnumTrackProgression::OnlyQueue: 0331 case AmarokConfig::EnumTrackProgression::Normal: 0332 default: 0333 m_navigator = new StandardTrackNavigator(); 0334 break; 0335 } 0336 0337 m_navigator->queueIds( currentQueue ); 0338 0339 Q_EMIT navigatorChanged(); 0340 } 0341 0342 void 0343 Playlist::Actions::repopulateDynamicPlaylist() 0344 { 0345 DEBUG_BLOCK 0346 0347 if ( typeid( *m_navigator ) == typeid( DynamicTrackNavigator ) ) 0348 { 0349 static_cast<DynamicTrackNavigator*>(m_navigator)->repopulate(); 0350 } 0351 } 0352 0353 void 0354 Playlist::Actions::shuffle() 0355 { 0356 QList<int> fromRows, toRows; 0357 0358 { 0359 const int rowCount = The::playlist()->qaim()->rowCount(); 0360 fromRows.reserve( rowCount ); 0361 0362 QMultiMap<int, int> shuffleToRows; 0363 for( int row = 0; row < rowCount; ++row ) 0364 { 0365 fromRows.append( row ); 0366 shuffleToRows.insert( QRandomGenerator::global()->generate(), row ); 0367 } 0368 toRows = shuffleToRows.values(); 0369 } 0370 0371 The::playlistController()->reorderRows( fromRows, toRows ); 0372 } 0373 0374 int 0375 Playlist::Actions::queuePosition( quint64 id ) 0376 { 0377 return m_navigator->queuePosition( id ); 0378 } 0379 0380 QQueue<quint64> 0381 Playlist::Actions::queue() 0382 { 0383 return m_navigator->queue(); 0384 } 0385 0386 bool 0387 Playlist::Actions::queueMoveUp( quint64 id ) 0388 { 0389 const bool ret = m_navigator->queueMoveUp( id ); 0390 if ( ret ) 0391 Playlist::ModelStack::instance()->bottom()->emitQueueChanged(); 0392 return ret; 0393 } 0394 0395 bool 0396 Playlist::Actions::queueMoveDown( quint64 id ) 0397 { 0398 const bool ret = m_navigator->queueMoveDown( id ); 0399 if ( ret ) 0400 Playlist::ModelStack::instance()->bottom()->emitQueueChanged(); 0401 return ret; 0402 } 0403 0404 void 0405 Playlist::Actions::dequeue( quint64 id ) 0406 { 0407 m_navigator->dequeueId( id ); // has no return value, *shrug* 0408 Playlist::ModelStack::instance()->bottom()->emitQueueChanged(); 0409 return; 0410 } 0411 0412 void 0413 Playlist::Actions::queue( const QList<int> &rows ) 0414 { 0415 QList<quint64> ids; 0416 foreach( int row, rows ) 0417 ids << The::playlist()->idAt( row ); 0418 queue( ids ); 0419 } 0420 0421 void 0422 Playlist::Actions::queue( const QList<quint64> &ids ) 0423 { 0424 m_navigator->queueIds( ids ); 0425 if ( !ids.isEmpty() ) 0426 Playlist::ModelStack::instance()->bottom()->emitQueueChanged(); 0427 } 0428 0429 void 0430 Playlist::Actions::dequeue( const QList<int> &rows ) 0431 { 0432 DEBUG_BLOCK 0433 0434 foreach( int row, rows ) 0435 { 0436 quint64 id = The::playlist()->idAt( row ); 0437 m_navigator->dequeueId( id ); 0438 } 0439 if ( !rows.isEmpty() ) 0440 Playlist::ModelStack::instance()->bottom()->emitQueueChanged(); 0441 } 0442 0443 void 0444 Playlist::Actions::slotTrackPlaying( Meta::TrackPtr engineTrack ) 0445 { 0446 DEBUG_BLOCK 0447 0448 if ( engineTrack ) 0449 { 0450 Meta::TrackPtr candidateTrack = The::playlist()->trackForId( m_nextTrackCandidate ); // May be 0. 0451 if ( engineTrack == candidateTrack ) 0452 { // The engine is playing what we planned: everything is OK. 0453 The::playlist()->setActiveId( m_nextTrackCandidate ); 0454 } 0455 else 0456 { 0457 warning() << "engineNewTrackPlaying:" << engineTrack->prettyName() << "does not match what the playlist controller thought it should be"; 0458 if ( The::playlist()->activeTrack() != engineTrack ) 0459 { 0460 // this will set active row to -1 if the track isn't in the playlist at all 0461 int row = The::playlist()->firstRowForTrack( engineTrack ); 0462 if( row != -1 ) 0463 The::playlist()->setActiveRow( row ); 0464 else 0465 The::playlist()->setActiveRow( AmarokConfig::lastPlaying() ); 0466 } 0467 //else 0468 // Engine and playlist are in sync even though we didn't plan it; do nothing 0469 } 0470 } 0471 else 0472 warning() << "engineNewTrackPlaying: not really a track"; 0473 0474 m_nextTrackCandidate = 0; 0475 } 0476 0477 void 0478 Playlist::Actions::slotPlayingStopped( qint64 finalPosition, qint64 trackLength ) 0479 { 0480 DEBUG_BLOCK; 0481 0482 stopAfterPlayingTrack( 0 ); // reset possible "Stop after playing track"; 0483 0484 // we have to determine if we reached the end of the playlist. 0485 // in such a case there would be no new track and the current one 0486 // played until the end. 0487 // else this must be a result of StopAfterCurrent or the user stopped 0488 if( m_nextTrackCandidate || finalPosition < trackLength ) 0489 return; 0490 0491 debug() << "nothing more to play..."; 0492 // no more stuff to play. make sure to reset the active track so that pressing play 0493 // will start at the top of the playlist (or whereever the navigator wants to start) 0494 // instead of just replaying the last track 0495 The::playlist()->setActiveRow( -1 ); 0496 0497 // we also need to mark all tracks as unplayed or some navigators might be unhappy 0498 The::playlist()->setAllUnplayed(); 0499 } 0500 0501 void 0502 Playlist::Actions::normalizeDynamicPlaylist() 0503 { 0504 if ( typeid( *m_navigator ) == typeid( DynamicTrackNavigator ) ) 0505 { 0506 static_cast<DynamicTrackNavigator*>(m_navigator)->appendUpcoming(); 0507 } 0508 } 0509 0510 void 0511 Playlist::Actions::repaintPlaylist() 0512 { 0513 The::mainWindow()->playlistDock()->currentView()->update(); 0514 } 0515 0516 void 0517 Playlist::Actions::restoreDefaultPlaylist() 0518 { 0519 DEBUG_BLOCK 0520 0521 // The PlaylistManager needs to be loaded or podcast episodes and other 0522 // non-collection Tracks will not be loaded correctly. 0523 The::playlistManager(); 0524 Playlist::Restorer *restorer = new Playlist::Restorer(); 0525 restorer->restore( QUrl::fromLocalFile(Amarok::defaultPlaylistPath()) ); 0526 connect( restorer, &Playlist::Restorer::restoreFinished, restorer, &QObject::deleteLater ); 0527 } 0528 0529 namespace The 0530 { 0531 AMAROK_EXPORT Playlist::Actions* playlistActions() { return Playlist::Actions::instance(); } 0532 }