1 /****************************************************************************************
2  * Copyright (c) 2007-2008 Ian Monroe <ian@monroe.nu>                                   *
3  * Copyright (c) 2007-2009 Nikolaj Hald Nielsen <nhn@kde.org>                           *
4  * Copyright (c) 2008 Seb Ruiz <ruiz@kde.org>                                           *
5  * Copyright (c) 2008 Soren Harward <stharward@gmail.com>                               *
6  * Copyright (c) 2009 Téo Mrnjavac <teo@kde.org>                                        *
7  *                                                                                      *
8  * This program is free software; you can redistribute it and/or modify it under        *
9  * the terms of the GNU General Public License as published by the Free Software        *
10  * Foundation; either version 2 of the License, or (at your option) version 3 or        *
11  * any later version accepted by the membership of KDE e.V. (or its successor approved  *
12  * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of  *
13  * version 3 of the license.                                                            *
14  *                                                                                      *
15  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
16  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
17  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
18  *                                                                                      *
19  * You should have received a copy of the GNU General Public License along with         *
20  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
21  ****************************************************************************************/
22 
23 #define DEBUG_PREFIX "Playlist::Actions"
24 
25 #include "PlaylistActions.h"
26 
27 #include "EngineController.h"
28 #include "MainWindow.h"
29 #include "amarokconfig.h"
30 #include "core/support/Amarok.h"
31 #include "core/support/Components.h"
32 #include "core/support/Debug.h"
33 #include "core/logger/Logger.h"
34 #include "core-impl/collections/support/CollectionManager.h"
35 #include "core-impl/playlists/types/file/PlaylistFileSupport.h"
36 #include "dynamic/DynamicModel.h"
37 #include "navigators/DynamicTrackNavigator.h"
38 #include "navigators/RandomAlbumNavigator.h"
39 #include "navigators/RandomTrackNavigator.h"
40 #include "navigators/RepeatAlbumNavigator.h"
41 #include "navigators/RepeatTrackNavigator.h"
42 #include "navigators/StandardTrackNavigator.h"
43 #include "navigators/FavoredRandomTrackNavigator.h"
44 #include "playlist/PlaylistController.h"
45 #include "playlist/PlaylistDock.h"
46 #include "playlist/PlaylistModelStack.h"
47 #include "playlist/PlaylistRestorer.h"
48 #include "playlistmanager/PlaylistManager.h"
49 
50 #include <QStandardPaths>
51 #include <typeinfo>
52 
53 Playlist::Actions* Playlist::Actions::s_instance = nullptr;
54 
instance()55 Playlist::Actions* Playlist::Actions::instance()
56 {
57     if( !s_instance )
58     {
59         s_instance = new Actions();
60         s_instance->init(); // prevent infinite recursion by using the playlist actions only after setting the instance.
61     }
62     return s_instance;
63 }
64 
65 void
destroy()66 Playlist::Actions::destroy()
67 {
68     delete s_instance;
69     s_instance = nullptr;
70 }
71 
Actions()72 Playlist::Actions::Actions()
73         : QObject()
74         , m_nextTrackCandidate( 0 )
75         , m_stopAfterPlayingTrackId( 0 )
76         , m_navigator( 0 )
77         , m_waitingForNextTrack( false )
78 {
79     EngineController *engine = The::engineController();
80 
81     if( engine ) // test cases might create a playlist without having an EngineController
82     {
83         connect( engine, &EngineController::trackPlaying,
84                  this, &Playlist::Actions::slotTrackPlaying );
85         connect( engine, &EngineController::stopped,
86                  this, &Playlist::Actions::slotPlayingStopped );
87     }
88 }
89 
~Actions()90 Playlist::Actions::~Actions()
91 {
92     delete m_navigator;
93 }
94 
95 void
init()96 Playlist::Actions::init()
97 {
98     playlistModeChanged(); // sets m_navigator.
99     restoreDefaultPlaylist();
100 }
101 
102 Meta::TrackPtr
likelyNextTrack()103 Playlist::Actions::likelyNextTrack()
104 {
105     return The::playlist()->trackForId( m_navigator->likelyNextTrack() );
106 }
107 
108 Meta::TrackPtr
likelyPrevTrack()109 Playlist::Actions::likelyPrevTrack()
110 {
111     return The::playlist()->trackForId( m_navigator->likelyLastTrack() );
112 }
113 
114 void
requestNextTrack()115 Playlist::Actions::requestNextTrack()
116 {
117     DEBUG_BLOCK
118     if ( m_nextTrackCandidate != 0 )
119         return;
120 
121     m_nextTrackCandidate = m_navigator->requestNextTrack();
122     if( m_nextTrackCandidate == 0 )
123         return;
124 
125     if( willStopAfterTrack( ModelStack::instance()->bottom()->activeId() ) )
126         // Tell playlist what track to play after users hits Play again:
127         The::playlist()->setActiveId( m_nextTrackCandidate );
128     else
129         play( m_nextTrackCandidate, false );
130 }
131 
132 void
requestUserNextTrack()133 Playlist::Actions::requestUserNextTrack()
134 {
135     m_nextTrackCandidate = m_navigator->requestUserNextTrack();
136     play( m_nextTrackCandidate );
137 }
138 
139 void
requestPrevTrack()140 Playlist::Actions::requestPrevTrack()
141 {
142     m_nextTrackCandidate = m_navigator->requestLastTrack();
143     play( m_nextTrackCandidate );
144 }
145 
146 void
requestTrack(quint64 id)147 Playlist::Actions::requestTrack( quint64 id )
148 {
149     m_nextTrackCandidate = id;
150 }
151 
152 void
stopAfterPlayingTrack(quint64 id)153 Playlist::Actions::stopAfterPlayingTrack( quint64 id )
154 {
155     if( id == quint64( -1 ) )
156         id = ModelStack::instance()->bottom()->activeId(); // 0 is fine
157     if( id != m_stopAfterPlayingTrackId )
158     {
159         m_stopAfterPlayingTrackId = id;
160         repaintPlaylist(); // to get the visual change
161     }
162 }
163 
164 bool
willStopAfterTrack(const quint64 id) const165 Playlist::Actions::willStopAfterTrack( const quint64 id ) const
166 {
167     return m_stopAfterPlayingTrackId && m_stopAfterPlayingTrackId == id;
168 }
169 
170 void
play()171 Playlist::Actions::play()
172 {
173     DEBUG_BLOCK
174 
175     if ( m_nextTrackCandidate == 0 )
176     {
177         m_nextTrackCandidate = The::playlist()->activeId();
178         // the queue has priority, and requestNextTrack() respects the queue.
179         // this is a bit of a hack because we "know" that all navigators will look at the queue first.
180         if ( !m_nextTrackCandidate || !m_navigator->queue().isEmpty() )
181             m_nextTrackCandidate = m_navigator->requestNextTrack();
182     }
183 
184     play( m_nextTrackCandidate );
185 }
186 
187 void
play(const QModelIndex & index)188 Playlist::Actions::play( const QModelIndex &index )
189 {
190     DEBUG_BLOCK
191 
192     if( index.isValid() )
193     {
194         m_nextTrackCandidate = index.data( UniqueIdRole ).value<quint64>();
195         play( m_nextTrackCandidate );
196     }
197 }
198 
199 void
play(const int row)200 Playlist::Actions::play( const int row )
201 {
202     DEBUG_BLOCK
203 
204     m_nextTrackCandidate = The::playlist()->idAt( row );
205     play( m_nextTrackCandidate );
206 }
207 
208 void
play(const quint64 trackid,bool now)209 Playlist::Actions::play( const quint64 trackid, bool now )
210 {
211     DEBUG_BLOCK
212 
213     Meta::TrackPtr track = The::playlist()->trackForId( trackid );
214     if ( track )
215     {
216         if ( now )
217             The::engineController()->play( track );
218         else
219             The::engineController()->setNextTrack( track );
220     }
221     else
222     {
223         warning() << "Invalid trackid" << trackid;
224     }
225 }
226 
227 void
next()228 Playlist::Actions::next()
229 {
230     DEBUG_BLOCK
231     requestUserNextTrack();
232 }
233 
234 void
back()235 Playlist::Actions::back()
236 {
237     DEBUG_BLOCK
238     requestPrevTrack();
239 }
240 
241 
242 void
enableDynamicMode(bool enable)243 Playlist::Actions::enableDynamicMode( bool enable )
244 {
245     if( AmarokConfig::dynamicMode() == enable )
246         return;
247 
248     AmarokConfig::setDynamicMode( enable );
249     // TODO: turn off other incompatible modes
250     // TODO: should we restore the state of other modes?
251     AmarokConfig::self()->save();
252 
253     Playlist::Dock *dock = The::mainWindow()->playlistDock();
254     Playlist::SortWidget *sorting = dock ? dock->sortWidget() : 0;
255     if( sorting )
256         sorting->trimToLevel();
257 
258     playlistModeChanged();
259 
260     /* append upcoming tracks to satisfy user's with about number of upcoming tracks.
261      * Needs to be _after_ playlistModeChanged() because before calling it the old
262      * m_navigator still reigns. */
263     if( enable )
264         normalizeDynamicPlaylist();
265 }
266 
267 
268 void
playlistModeChanged()269 Playlist::Actions::playlistModeChanged()
270 {
271     DEBUG_BLOCK
272 
273     QQueue<quint64> currentQueue;
274 
275     if ( m_navigator )
276     {
277         //HACK: Migrate the queue to the new navigator
278         //TODO: The queue really should not be maintained by the navigators in this way
279         // but should be handled by a separate and persistent object.
280 
281         currentQueue = m_navigator->queue();
282         m_navigator->deleteLater();
283     }
284 
285     debug() << "Dynamic mode:   " << AmarokConfig::dynamicMode();
286 
287     if ( AmarokConfig::dynamicMode() )
288     {
289         m_navigator = new DynamicTrackNavigator();
290         Q_EMIT navigatorChanged();
291         return;
292     }
293 
294     m_navigator = 0;
295 
296     switch( AmarokConfig::trackProgression() )
297     {
298 
299         case AmarokConfig::EnumTrackProgression::RepeatTrack:
300             m_navigator = new RepeatTrackNavigator();
301             break;
302 
303         case AmarokConfig::EnumTrackProgression::RepeatAlbum:
304             m_navigator = new RepeatAlbumNavigator();
305             break;
306 
307         case AmarokConfig::EnumTrackProgression::RandomTrack:
308             switch( AmarokConfig::favorTracks() )
309             {
310                 case AmarokConfig::EnumFavorTracks::HigherScores:
311                 case AmarokConfig::EnumFavorTracks::HigherRatings:
312                 case AmarokConfig::EnumFavorTracks::LessRecentlyPlayed:
313                     m_navigator = new FavoredRandomTrackNavigator();
314                     break;
315 
316                 case AmarokConfig::EnumFavorTracks::Off:
317                 default:
318                     m_navigator = new RandomTrackNavigator();
319                     break;
320             }
321             break;
322 
323         case AmarokConfig::EnumTrackProgression::RandomAlbum:
324             m_navigator = new RandomAlbumNavigator();
325             break;
326 
327         //repeat playlist, standard, only queue and fallback are all the normal navigator.
328         case AmarokConfig::EnumTrackProgression::RepeatPlaylist:
329         case AmarokConfig::EnumTrackProgression::OnlyQueue:
330         case AmarokConfig::EnumTrackProgression::Normal:
331         default:
332             m_navigator = new StandardTrackNavigator();
333             break;
334     }
335 
336     m_navigator->queueIds( currentQueue );
337 
338     Q_EMIT navigatorChanged();
339 }
340 
341 void
repopulateDynamicPlaylist()342 Playlist::Actions::repopulateDynamicPlaylist()
343 {
344     DEBUG_BLOCK
345 
346     if ( typeid( *m_navigator ) == typeid( DynamicTrackNavigator ) )
347     {
348         static_cast<DynamicTrackNavigator*>(m_navigator)->repopulate();
349     }
350 }
351 
352 void
shuffle()353 Playlist::Actions::shuffle()
354 {
355     QList<int> fromRows, toRows;
356 
357     {
358         const int rowCount = The::playlist()->qaim()->rowCount();
359         fromRows.reserve( rowCount );
360 
361         QMultiMap<int, int> shuffleToRows;
362         for( int row = 0; row < rowCount; ++row )
363         {
364             fromRows.append( row );
365             shuffleToRows.insert( qrand(), row );
366         }
367         toRows = shuffleToRows.values();
368     }
369 
370     The::playlistController()->reorderRows( fromRows, toRows );
371 }
372 
373 int
queuePosition(quint64 id)374 Playlist::Actions::queuePosition( quint64 id )
375 {
376     return m_navigator->queuePosition( id );
377 }
378 
379 QQueue<quint64>
queue()380 Playlist::Actions::queue()
381 {
382     return m_navigator->queue();
383 }
384 
385 bool
queueMoveUp(quint64 id)386 Playlist::Actions::queueMoveUp( quint64 id )
387 {
388     const bool ret = m_navigator->queueMoveUp( id );
389     if ( ret )
390         Playlist::ModelStack::instance()->bottom()->emitQueueChanged();
391     return ret;
392 }
393 
394 bool
queueMoveDown(quint64 id)395 Playlist::Actions::queueMoveDown( quint64 id )
396 {
397     const bool ret = m_navigator->queueMoveDown( id );
398     if ( ret )
399         Playlist::ModelStack::instance()->bottom()->emitQueueChanged();
400     return ret;
401 }
402 
403 void
dequeue(quint64 id)404 Playlist::Actions::dequeue( quint64 id )
405 {
406     m_navigator->dequeueId( id ); // has no return value, *shrug*
407     Playlist::ModelStack::instance()->bottom()->emitQueueChanged();
408     return;
409 }
410 
411 void
queue(const QList<int> & rows)412 Playlist::Actions::queue( const QList<int> &rows )
413 {
414     QList<quint64> ids;
415     foreach( int row, rows )
416         ids << The::playlist()->idAt( row );
417     queue( ids );
418 }
419 
420 void
queue(const QList<quint64> & ids)421 Playlist::Actions::queue( const QList<quint64> &ids )
422 {
423     m_navigator->queueIds( ids );
424     if ( !ids.isEmpty() )
425         Playlist::ModelStack::instance()->bottom()->emitQueueChanged();
426 }
427 
428 void
dequeue(const QList<int> & rows)429 Playlist::Actions::dequeue( const QList<int> &rows )
430 {
431     DEBUG_BLOCK
432 
433     foreach( int row, rows )
434     {
435         quint64 id = The::playlist()->idAt( row );
436         m_navigator->dequeueId( id );
437     }
438     if ( !rows.isEmpty() )
439         Playlist::ModelStack::instance()->bottom()->emitQueueChanged();
440 }
441 
442 void
slotTrackPlaying(Meta::TrackPtr engineTrack)443 Playlist::Actions::slotTrackPlaying( Meta::TrackPtr engineTrack )
444 {
445     DEBUG_BLOCK
446 
447     if ( engineTrack )
448     {
449         Meta::TrackPtr candidateTrack = The::playlist()->trackForId( m_nextTrackCandidate );    // May be 0.
450         if ( engineTrack == candidateTrack )
451         {   // The engine is playing what we planned: everything is OK.
452             The::playlist()->setActiveId( m_nextTrackCandidate );
453         }
454         else
455         {
456             warning() << "engineNewTrackPlaying:" << engineTrack->prettyName() << "does not match what the playlist controller thought it should be";
457             if ( The::playlist()->activeTrack() != engineTrack )
458             {
459                  // this will set active row to -1 if the track isn't in the playlist at all
460                 int row = The::playlist()->firstRowForTrack( engineTrack );
461                 if( row != -1 )
462                     The::playlist()->setActiveRow( row );
463                 else
464                     The::playlist()->setActiveRow( AmarokConfig::lastPlaying() );
465             }
466             //else
467             //  Engine and playlist are in sync even though we didn't plan it; do nothing
468         }
469     }
470     else
471         warning() << "engineNewTrackPlaying: not really a track";
472 
473     m_nextTrackCandidate = 0;
474 }
475 
476 void
slotPlayingStopped(qint64 finalPosition,qint64 trackLength)477 Playlist::Actions::slotPlayingStopped( qint64 finalPosition, qint64 trackLength )
478 {
479     DEBUG_BLOCK;
480 
481     stopAfterPlayingTrack( 0 ); // reset possible "Stop after playing track";
482 
483     // we have to determine if we reached the end of the playlist.
484     // in such a case there would be no new track and the current one
485     // played until the end.
486     // else this must be a result of StopAfterCurrent or the user stopped
487     if( m_nextTrackCandidate || finalPosition < trackLength )
488         return;
489 
490     debug() << "nothing more to play...";
491     // no more stuff to play. make sure to reset the active track so that pressing play
492     // will start at the top of the playlist (or whereever the navigator wants to start)
493     // instead of just replaying the last track
494     The::playlist()->setActiveRow( -1 );
495 
496     // we also need to mark all tracks as unplayed or some navigators might be unhappy
497     The::playlist()->setAllUnplayed();
498 }
499 
500 void
normalizeDynamicPlaylist()501 Playlist::Actions::normalizeDynamicPlaylist()
502 {
503     if ( typeid( *m_navigator ) == typeid( DynamicTrackNavigator ) )
504     {
505         static_cast<DynamicTrackNavigator*>(m_navigator)->appendUpcoming();
506     }
507 }
508 
509 void
repaintPlaylist()510 Playlist::Actions::repaintPlaylist()
511 {
512     The::mainWindow()->playlistDock()->currentView()->update();
513 }
514 
515 void
restoreDefaultPlaylist()516 Playlist::Actions::restoreDefaultPlaylist()
517 {
518     DEBUG_BLOCK
519 
520     // The PlaylistManager needs to be loaded or podcast episodes and other
521     // non-collection Tracks will not be loaded correctly.
522     The::playlistManager();
523     Playlist::Restorer *restorer = new Playlist::Restorer();
524     restorer->restore( QUrl::fromLocalFile(Amarok::defaultPlaylistPath()) );
525     connect( restorer, &Playlist::Restorer::restoreFinished, restorer, &QObject::deleteLater );
526 }
527 
528 namespace The
529 {
playlistActions()530     AMAROK_EXPORT Playlist::Actions* playlistActions() { return Playlist::Actions::instance(); }
531 }
532