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