Music Hub ..
A session-wide music playback service
Loading...
Searching...
No Matches
track_list_implementation.cpp
Go to the documentation of this file.
1/*
2 * Copyright © 2013-2015 Canonical Ltd.
3 * Copyright © 2022 UBports Foundation.
4 *
5 * Contact: Alberto Mardegan <mardy@users.sourceforge.net>
6 *
7 * This program is free software: you can redistribute it and/or modify it
8 * under the terms of the GNU Lesser General Public License version 3,
9 * as published by the Free Software Foundation.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 *
19 * Authored by: Thomas Voß <thomas.voss@canonical.com>
20 */
21
23
24#include "engine.h"
25#include "logging.h"
26
27#include <QMap>
28#include <QPair>
29#include <QRandomGenerator>
30#include <QSharedPointer>
31#include <QUrl>
32
33#include <algorithm>
34#include <sstream>
35#include <stdio.h>
36#include <stdlib.h>
37#include <tuple>
38#include <unistd.h>
39
41
42using namespace media;
43
44namespace lomiri {
45namespace MediaHubService {
46
48{
49 Q_DECLARE_PUBLIC(TrackListImplementation)
50
51public:
52 typedef QMap<Track::Id, QPair<QUrl, Track::MetaData>> MetaDataCache;
53
55 const QSharedPointer<media::Engine::MetaDataExtractor> &extractor,
57
60 return it == m_tracks.begin();
61 }
63 return it == m_tracks.end();
64 }
65
67 void set_current_track(const Track::Id &id);
70
71 void add_track_with_uri_at(const QUrl &uri,
72 const Track::Id &position,
73 bool make_current);
74 void add_tracks_with_uri_at(const QVector<QUrl> &uris,
75 const Track::Id &position);
76 bool move_track(const Track::Id &id, const Track::Id &to);
77 void remove_track(const Track::Id &id);
78 void do_remove_track(const Track::Id &id);
79 void go_to(const Track::Id &track);
80 void updateCachedTrackMetadata(const Track::Id &id, const QUrl &uri)
81 {
82 auto i = meta_data_cache.find(id);
83 if (i == meta_data_cache.end()) {
84 meta_data_cache[id] = qMakePair(uri, Track::MetaData{});
85 } else {
86 i.value().first = uri;
87 }
88 }
89
90 media::TrackList::Container::iterator get_shuffled_insert_it()
91 {
92 auto random_it = shuffled_tracks.begin();
93 if (random_it == shuffled_tracks.end())
94 return random_it;
95
96 uint32_t random = QRandomGenerator::global()->generate();
97 // This is slightly biased, but not much, as RAND_MAX >= 32767, which is
98 // much more than the average number of tracks.
99 // Note that for N tracks we have N + 1 possible insertion positions.
100 std::advance(random_it, random % (shuffled_tracks.count() + 1));
101 return random_it;
102 }
103
106 QSharedPointer<Engine::MetaDataExtractor> extractor;
107 // Used for caching the original tracklist order to be used to restore the order
108 // to the live TrackList after shuffle is turned off
116};
117
118}} // namespace
119
121 const QSharedPointer<media::Engine::MetaDataExtractor> &extractor,
123 track_counter(0),
125 shuffle(false),
126 loop_status(media::Player::LoopStatus::none),
128 q_ptr(q)
129{
130}
131
134{
135 // Prevent the TrackList from sitting at the end which will cause
136 // a segfault when calling current()
137 if (!m_tracks.isEmpty() && current_track.isEmpty())
138 {
139 MH_DEBUG("Wrapping d->current_track back to begin()");
140 current_track = m_tracks.first();
141 }
142 else if (m_tracks.isEmpty())
143 {
144 MH_ERROR("TrackList is empty therefore there is no valid current track");
145 }
146
147 return std::find(m_tracks.begin(), m_tracks.end(), current_track);
148}
149
151{
152 if (m_tracks.contains(id))
153 current_track = id;
154}
155
157{
158 if (m_tracks.isEmpty())
159 return Track::Id{};
160
161 return current_track;
162}
163
166{
167 auto current_id = get_current_track();
168 return std::find(shuffled_tracks.begin(), shuffled_tracks.end(), current_id);
169}
170
172 const QUrl &uri,
173 const media::Track::Id& position,
174 bool make_current)
175{
177 MH_TRACE("");
178
179 Track::Id id = q->objectName() + '/' + QString::number(track_counter++);
180
181 MH_DEBUG("Adding Track::Id: %s", qUtf8Printable(id));
182 MH_DEBUG("\tURI: %s", qUtf8Printable(uri.toString()));
183
184 const auto current = get_current_track();
185
186 auto it = std::find(m_tracks.begin(), m_tracks.end(), position);
187 m_tracks.insert(it, id);
188
190
191 if (shuffle)
193
194 if (make_current) {
196 go_to(id);
197 } else {
198 set_current_track(current);
199 }
200
201 MH_DEBUG("Signaling that we just added track id: %s", qUtf8Printable(id));
202 // Signal to the client that a track was added to the TrackList
203 Q_EMIT q->trackAdded(id);
204
205 // Signal to the client that the current track has changed for the first
206 // track added to the TrackList
207 if (m_tracks.count() == 1) {
208 Q_EMIT q->trackChanged(id);
209 }
210}
211
213 const QVector<QUrl> &uris,
214 const Track::Id &position)
215{
217
218 const auto current = get_current_track();
219
220 Track::Id current_id;
221 QVector<QUrl> tmp;
222 for (const auto uri : uris)
223 {
224 // TODO: Refactor this code to use a smaller common function shared with add_track_with_uri_at()
225 Track::Id id = q->objectName() + '/' +
226 QString::number(track_counter++);
227 MH_DEBUG("Adding Track::Id: %s", qUtf8Printable(id));
228 MH_DEBUG("\tURI: %s", qUtf8Printable(uri.toString()));
229
230 tmp.push_back(id);
231
232 Track::Id insert_position = position;
233
234 auto it = std::find(m_tracks.begin(), m_tracks.end(), insert_position);
235 m_tracks.insert(it, id);
236 // Make sure the next insert position is after the current insert position
237 // Update the Track::Id after which to insert the next one from uris
238 insert_position = id;
239
241
242 if (shuffle)
244
245 if (m_tracks.count() == 1)
246 current_id = id;
247 }
248
249 set_current_track(current);
250
251 MH_DEBUG("Signaling that we just added %d tracks to the TrackList", tmp.size());
252 Q_EMIT q->tracksAdded(tmp);
253
254 if (!current_id.isEmpty()) {
255 Q_EMIT q->trackChanged(current_id);
256 }
257}
258
260 const media::Track::Id &to)
261{
263 MH_DEBUG("-----------------------------------------------------");
264 if (id.isEmpty() or to.isEmpty())
265 {
266 MH_ERROR("Can't move track since 'id' or 'to' are empty");
267 return false;
268 }
269
270 if (id == to)
271 {
272 MH_ERROR("Can't move track to it's same position");
273 return false;
274 }
275
276 if (m_tracks.count() == 1)
277 {
278 MH_ERROR("Can't move track since TrackList contains only one track");
279 return false;
280 }
281
282 MH_DEBUG("current_track id: %s", qUtf8Printable(current_track));
283 // Get an iterator that points to the track that is the insertion point
284 auto insert_point_it = std::find(m_tracks.begin(), m_tracks.end(), to);
285 if (insert_point_it == m_tracks.end()) {
287 ("Failed to find source track " + id);
288 }
289
290 // Get an iterator that points to the track to move within the TrackList
291 auto to_move_it = std::find(m_tracks.begin(), m_tracks.end(), id);
292 if (to_move_it != m_tracks.end())
293 {
294 m_tracks.erase(to_move_it);
295 }
296 else
297 {
299 ("Failed to find destination track " + to);
300 }
301
302 // Insert id at the location just before insert_point_it
303 m_tracks.insert(insert_point_it, id);
304
305 const auto new_current_track_it = std::find(m_tracks.begin(), m_tracks.end(), current_track);
306 if (!current_track.isEmpty() && new_current_track_it == m_tracks.end()) {
307 MH_ERROR("Can't update current_iterator - failed to find track after move");
309 }
310
311 MH_DEBUG("TrackList after move");
312 for(const auto track : m_tracks)
313 {
314 MH_DEBUG("%s", qUtf8Printable(track));
315 }
316 // Signal to the client that track 'id' was moved within the TrackList
317 Q_EMIT q->trackMoved(id, to);
318
319 MH_DEBUG("-----------------------------------------------------");
320
321 return true;
322}
323
325{
327
328 media::Track::Id track = id;
329
330 auto id_it = std::find(m_tracks.begin(), m_tracks.end(), track);
331 if (id_it == m_tracks.end()) {
332 QString err_str = QString("Track ") + track + " not found in track list";
333 MH_WARNING() << err_str;
335 }
336
337 media::Track::Id next;
338 bool deleting_current = false;
339
340 if (id == current_track)
341 {
342 MH_DEBUG("Removing current track");
343 deleting_current = true;
344
345 auto next_it = std::next(id_it);
346
347 if (next_it == m_tracks.end() &&
349 {
350 // Removed the last track, current is the first track and make sure that
351 // the player starts playing it
352 next_it = m_tracks.begin();
353 }
354
355 if (next_it == m_tracks.end())
356 {
357 current_track.clear();
358 // Nothing else to play, stop playback
359 Q_EMIT q->endOfTrackList();
360 }
361 else
362 {
363 current_track = *next_it;
364 }
365 }
366
367 do_remove_track(track);
368
369 if (!current_track.isEmpty() and deleting_current)
371}
372
374{
376 int removed = m_tracks.removeAll(id);
377
378 if (removed > 0)
379 {
380 meta_data_cache.remove(id);
381
382 if (shuffle)
383 shuffled_tracks.removeAll(id);
384
385 Q_EMIT q->trackRemoved(id);
386
387 // Make sure playback stops if all tracks were removed
388 if (m_tracks.isEmpty())
389 Q_EMIT q->endOfTrackList();
390 }
391}
392
394{
396 set_current_track(track);
397 // Signal the Player instance to go to a specific track for playback
398 Q_EMIT q->onGoToTrack(track);
399 Q_EMIT q->trackChanged(track);
400}
401
403 const QSharedPointer<media::Engine::MetaDataExtractor> &extractor,
404 QObject *parent):
405 QObject(parent),
406 d_ptr(new TrackListImplementationPrivate(extractor, this))
407{
408}
409
413
415{
417 d->shuffle = shuffle;
418
419 if (shuffle) {
420 d->shuffled_tracks = d->m_tracks;
421 std::random_shuffle(d->shuffled_tracks.begin(),
422 d->shuffled_tracks.end());
423 }
424}
425
427{
428 Q_D(const TrackListImplementation);
429 return d->shuffle;
430}
431
433{
435 const auto it = d->meta_data_cache.find(id);
436
437 if (it == d->meta_data_cache.end())
438 return QUrl();
439
440 return it.value().first;
441}
442
444{
446 const auto it = d->meta_data_cache.find(id);
447
448 if (it == d->meta_data_cache.end())
449 return Track::MetaData{};
450
451 return it.value().second;
452}
453
455 const QUrl &uri,
456 const media::Track::Id& position,
457 bool make_current)
458{
460 MH_TRACE("");
461 d->add_track_with_uri_at(uri, position, make_current);
462}
463
465 const Track::Id &position)
466{
468 MH_TRACE("");
469 d->add_tracks_with_uri_at(uris, position);
470}
471
473 const Track::Id &to)
474{
476 MH_TRACE("");
477 return d->move_track(id, to);
478}
479
481{
483 d->remove_track(id);
484}
485
487{
489 MH_TRACE("");
490 d->go_to(track);
491}
492
494{
495 Q_D(const TrackListImplementation);
496 return d->shuffled_tracks;
497}
498
500{
502 MH_TRACE("");
503
504 d->current_track.clear();
505
506 // Make sure playback stops
507 Q_EMIT endOfTrackList();
508 // And make sure there is no "current" track
509 d->m_tracks.clear();
510 d->track_counter = 0;
511 d->shuffled_tracks.clear();
512
513 Q_EMIT trackListReset();
514}
515
517{
518 Q_D(const TrackListImplementation);
519 return d->m_tracks;
520}
521
522/*
523 * NOTE We do not consider the loop status in this function due to the use of it
524 * we do in TrackListImplementation::next() (the function is used to know whether we
525 * need to wrap when looping is active).
526 */
528{
529 Q_D(const TrackListImplementation);
530 const auto n_tracks = d->m_tracks.count();
531
532 if (n_tracks == 0)
533 return false;
534
535 // TODO Using current_iterator() makes media-hub crash later. Logic for
536 // handling the iterators must be reviewed. As a minimum updates to the
537 // track list should update current_track instead of the list being sneakly
538 // changed in player_implementation.cpp.
539 // To avoid the crash we consider that current_track will be eventually
540 // initialized to the first track when current_iterator() gets called.
541 if (d->current_track == d->empty_iterator())
542 {
543 if (n_tracks < 2)
544 return false;
545 else
546 return true;
547 }
548
549 if (shuffle())
550 {
551 auto it = d->get_current_shuffled();
552 return ++it != d->shuffled_tracks.end();
553 }
554 else
555 {
556 const auto next_track = std::next(d->current_iterator());
557 return !d->is_last_track(next_track);
558 }
559}
560
561/*
562 * NOTE We do not consider the loop status in this function due to the use of it
563 * we do in TrackListImplementation::previous() (the function is used to know whether we
564 * need to wrap when looping is active).
565 */
567{
568 Q_D(const TrackListImplementation);
569 if (d->m_tracks.isEmpty() || d->current_track == d->empty_iterator())
570 return false;
571
572 MH_DEBUG() << "shuffle is" << shuffle();
573 if (shuffle())
574 return d->get_current_shuffled() != d->shuffled_tracks.begin();
575 else
576 return d->current_track != d->m_tracks.begin();
577}
578
580{
582 MH_TRACE("");
583 if (d->m_tracks.isEmpty()) {
584 // TODO Change ServiceSkeleton to return with error from DBus call
585 MH_ERROR("No tracks, cannot go to next");
586 return media::Track::Id{};
587 }
588
589 bool go_to_track = false;
590
591 // End of the track reached so loop around to the beginning of the track
592 if (d->loop_status == media::Player::LoopStatus::track)
593 {
594 MH_INFO("Looping on the current track since LoopStatus is set to track");
595 go_to_track = true;
596 }
597 // End of the tracklist reached so loop around to the beginning of the tracklist
598 else if (d->loop_status == media::Player::LoopStatus::playlist && not hasNext())
599 {
600 MH_INFO("Looping on the tracklist since LoopStatus is set to playlist");
601
602 if (shuffle())
603 {
604 /* Re-shuffle the tracks, but make sure that the new first track
605 * is not the same as the current one */
606 const auto currentId = d->get_current_track();
607 std::random_shuffle(d->shuffled_tracks.begin(),
608 d->shuffled_tracks.end());
609 auto id = *d->shuffled_tracks.begin();
610 if (id == currentId) {
611 std::iter_swap(d->shuffled_tracks.begin(),
612 d->shuffled_tracks.end() - 1);
613 id = *d->shuffled_tracks.begin();
614 }
615 d->set_current_track(id);
616 }
617 else
618 {
619 d->current_track = d->m_tracks.first();
620 }
621 go_to_track = true;
622 }
623 else
624 {
625 if (shuffle())
626 {
627 auto it = d->get_current_shuffled();
628 if (it == d->shuffled_tracks.end()) {
629 d->set_current_track(d->shuffled_tracks.first());
630 go_to_track = true;
631 } else if (++it != d->shuffled_tracks.end()) {
632 MH_INFO("Advancing to next track: %s", qUtf8Printable(*it));
633 d->set_current_track(*it);
634 go_to_track = true;
635 }
636 }
637 else
638 {
639 const auto it = std::next(d->current_iterator());
640 if (not d->is_last_track(it))
641 {
642 MH_INFO("Advancing to next track: %s", qUtf8Printable(*it));
643 d->current_track = *it;
644 go_to_track = true;
645 }
646 }
647
648 }
649
650 const media::Track::Id id = *(d->current_iterator());
651 if (go_to_track)
652 {
653 MH_DEBUG("next track id is %s", qUtf8Printable(id));
654 Q_EMIT trackChanged(id);
655 // Signal the PlayerImplementation to play the next track
656 Q_EMIT onGoToTrack(id);
657 }
658 else
659 {
660 // At the end of the tracklist and not set to loop
661 MH_INFO("End of tracklist reached");
662 Q_EMIT endOfTrackList();
663 }
664
665 return id;
666}
667
669{
671 MH_TRACE("");
672 if (d->m_tracks.isEmpty()) {
673 // TODO Change ServiceSkeleton to return with error from DBus call
674 MH_ERROR("No tracks, cannot go to previous");
675 return media::Track::Id{};
676 }
677
678 bool go_to_track = false;
679 // Position is measured in nanoseconds
680 const uint64_t max_position = 5 * UINT64_C(1000000000);
681
682 // If we're playing the current track for > max_position time then
683 // repeat it from the beginning
684 if (d->current_position > max_position)
685 {
686 MH_INFO("Repeating current track...");
687 go_to_track = true;
688 }
689 // Loop on the current track forever
690 else if (d->loop_status == media::Player::LoopStatus::track)
691 {
692 MH_INFO("Looping on the current track...");
693 go_to_track = true;
694 }
695 // Loop over the whole playlist and repeat
696 else if (d->loop_status == media::Player::LoopStatus::playlist && not hasPrevious())
697 {
698 MH_INFO("Looping on the entire TrackList...");
699
700 if (shuffle())
701 {
702 const auto id = *std::prev(d->shuffled_tracks.end());
703 d->set_current_track(id);
704 }
705 else
706 {
707 d->current_track = d->m_tracks.last();
708 }
709
710 go_to_track = true;
711 }
712 else
713 {
714 if (shuffle())
715 {
716 auto it = d->get_current_shuffled();
717 if (it != d->shuffled_tracks.begin()) {
718 d->set_current_track(*(--it));
719 go_to_track = true;
720 }
721 }
722 else if (not d->is_first_track(d->current_iterator()))
723 {
724 // Keep returning the previous track until the first track is reached
725 d->current_track = *std::prev(d->current_iterator());
726 go_to_track = true;
727 }
728 }
729
730 const media::Track::Id id = *(d->current_iterator());
731 if (go_to_track)
732 {
733 Q_EMIT trackChanged(id);
734 Q_EMIT onGoToTrack(id);
735 }
736 else
737 {
738 // At the beginning of the tracklist and not set to loop
739 MH_INFO("Beginning of tracklist reached");
740 Q_EMIT endOfTrackList();
741 }
742
743 return id;
744}
745
747{
748 Q_D(const TrackListImplementation);
749 return *(d->current_iterator());
750}
751
753{
755 d->loop_status = loop_status;
756}
757
759{
760 Q_D(const TrackListImplementation);
761 return d->loop_status;
762}
763
765{
767 d->current_position = position;
768}
769
771{
772 return true;
773}
774
776{
777 static const media::Track::Id id{"/org/mpris/MediaPlayer2/TrackList/NoTrack"};
778 return id;
779}
void updateCachedTrackMetadata(const Track::Id &id, const QUrl &uri)
bool is_last_track(const TrackList::ConstIterator &it) const
bool is_first_track(const TrackList::ConstIterator &it) const
QMap< Track::Id, QPair< QUrl, Track::MetaData > > MetaDataCache
void add_tracks_with_uri_at(const QVector< QUrl > &uris, const Track::Id &position)
bool move_track(const Track::Id &id, const Track::Id &to)
TrackListImplementationPrivate(const QSharedPointer< media::Engine::MetaDataExtractor > &extractor, TrackListImplementation *q)
void add_track_with_uri_at(const QUrl &uri, const Track::Id &position, bool make_current)
void add_tracks_with_uri_at(const QVector< QUrl > &uris, const Track::Id &position)
Track::MetaData query_meta_data_for_track(const Track::Id &id)
TrackListImplementation(const QSharedPointer< Engine::MetaDataExtractor > &extractor, QObject *parent=nullptr)
void add_track_with_uri_at(const QUrl &uri, const Track::Id &position, bool make_current)
bool move_track(const Track::Id &id, const Track::Id &to)
#define MH_TRACE(...)
Definition logging.h:37
#define MH_ERROR(...)
Definition logging.h:41
#define MH_INFO(...)
Definition logging.h:39
#define MH_WARNING(...)
Definition logging.h:40
#define MH_DEBUG(...)
Definition logging.h:38