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 }