1 /*
2 * Strawberry Music Player
3 * This file was part of Clementine.
4 * Copyright 2010, David Sansome <me@davidsansome.com>
5 * Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
6 *
7 * Strawberry is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * Strawberry is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
19 *
20 */
21
22 #include "config.h"
23
24 #include <cstdlib>
25 #include <memory>
26 #include <utility>
27 #include <algorithm>
28 #include <functional>
29 #include <iterator>
30 #include <type_traits>
31 #include <unordered_map>
32 #include <random>
33 #include <chrono>
34
35 #include <QtGlobal>
36 #include <QObject>
37 #include <QCoreApplication>
38 #include <QtConcurrent>
39 #include <QFuture>
40 #include <QFutureWatcher>
41 #include <QIODevice>
42 #include <QDataStream>
43 #include <QBuffer>
44 #include <QFile>
45 #include <QList>
46 #include <QMap>
47 #include <QHash>
48 #include <QSet>
49 #include <QMimeData>
50 #include <QVariant>
51 #include <QString>
52 #include <QStringList>
53 #include <QUrl>
54 #include <QFont>
55 #include <QBrush>
56 #include <QUndoStack>
57 #include <QUndoCommand>
58 #include <QAbstractListModel>
59 #include <QMutableListIterator>
60 #include <QFlags>
61 #include <QSettings>
62 #include <QtDebug>
63 #include <QTimer>
64
65 #include "core/application.h"
66 #include "core/logging.h"
67 #include "core/mimedata.h"
68 #include "core/tagreaderclient.h"
69 #include "core/song.h"
70 #include "core/timeconstants.h"
71 #include "collection/collection.h"
72 #include "collection/collectionbackend.h"
73 #include "collection/collectionplaylistitem.h"
74 #include "covermanager/albumcoverloader.h"
75 #include "queue/queue.h"
76 #include "playlist.h"
77 #include "playlistitem.h"
78 #include "playlistview.h"
79 #include "playlistsequence.h"
80 #include "playlistbackend.h"
81 #include "playlistfilter.h"
82 #include "playlistitemmimedata.h"
83 #include "playlistundocommands.h"
84 #include "songloaderinserter.h"
85 #include "songmimedata.h"
86 #include "songplaylistitem.h"
87 #include "tagreadermessages.pb.h"
88
89 #include "smartplaylists/playlistgenerator.h"
90 #include "smartplaylists/playlistgeneratorinserter.h"
91 #include "smartplaylists/playlistgeneratormimedata.h"
92
93 #include "internet/internetplaylistitem.h"
94 #include "internet/internetsongmimedata.h"
95
96 #include "radios/radioservice.h"
97 #include "radios/radiomimedata.h"
98 #include "radios/radioplaylistitem.h"
99
100 using namespace std::chrono_literals;
101
102 const char *Playlist::kCddaMimeType = "x-content/audio-cdda";
103 const char *Playlist::kRowsMimetype = "application/x-strawberry-playlist-rows";
104 const char *Playlist::kPlayNowMimetype = "application/x-strawberry-play-now";
105
106 const int Playlist::kInvalidSongPriority = 200;
107 const QRgb Playlist::kInvalidSongColor = qRgb(0xC0, 0xC0, 0xC0);
108
109 const int Playlist::kDynamicHistoryPriority = 100;
110 const QRgb Playlist::kDynamicHistoryColor = qRgb(0x80, 0x80, 0x80);
111
112 const char *Playlist::kSettingsGroup = "Playlist";
113
114 const char *Playlist::kPathType = "path_type";
115 const char *Playlist::kWriteMetadata = "write_metadata";
116
117 const int Playlist::kUndoStackSize = 20;
118 const int Playlist::kUndoItemLimit = 500;
119
120 const qint64 Playlist::kMinScrobblePointNsecs = 31LL * kNsecPerSec;
121 const qint64 Playlist::kMaxScrobblePointNsecs = 240LL * kNsecPerSec;
122
Playlist(PlaylistBackend * backend,TaskManager * task_manager,CollectionBackend * collection,const int id,const QString & special_type,const bool favorite,QObject * parent)123 Playlist::Playlist(PlaylistBackend *backend, TaskManager *task_manager, CollectionBackend *collection, const int id, const QString &special_type, const bool favorite, QObject *parent)
124 : QAbstractListModel(parent),
125 is_loading_(false),
126 proxy_(new PlaylistFilter(this)),
127 queue_(new Queue(this, this)),
128 timer_save_(new QTimer(this)),
129 backend_(backend),
130 task_manager_(task_manager),
131 collection_(collection),
132 id_(id),
133 favorite_(favorite),
134 current_is_paused_(false),
135 current_virtual_index_(-1),
136 is_shuffled_(false),
137 playlist_sequence_(nullptr),
138 ignore_sorting_(false),
139 undo_stack_(new QUndoStack(this)),
140 special_type_(special_type),
141 cancel_restore_(false),
142 scrobbled_(false),
143 scrobble_point_(-1),
144 editing_(-1),
145 auto_sort_(false),
146 sort_column_(Column_Title),
147 sort_order_(Qt::AscendingOrder) {
148
149 undo_stack_->setUndoLimit(kUndoStackSize);
150
151 QObject::connect(this, &Playlist::rowsInserted, this, &Playlist::PlaylistChanged);
152 QObject::connect(this, &Playlist::rowsRemoved, this, &Playlist::PlaylistChanged);
153
154 Restore();
155
156 proxy_->setSourceModel(this);
157 queue_->setSourceModel(this);
158
159 QObject::connect(queue_, &Queue::rowsAboutToBeRemoved, this, &Playlist::TracksAboutToBeDequeued);
160 QObject::connect(queue_, &Queue::rowsRemoved, this, &Playlist::TracksDequeued);
161
162 QObject::connect(queue_, &Queue::rowsInserted, this, &Playlist::TracksEnqueued);
163
164 QObject::connect(queue_, &Queue::layoutChanged, this, &Playlist::QueueLayoutChanged);
165
166 QObject::connect(timer_save_, &QTimer::timeout, this, &Playlist::Save);
167
168 column_alignments_ = PlaylistView::DefaultColumnAlignment();
169
170 timer_save_->setSingleShot(true);
171 timer_save_->setInterval(900ms);
172
173 }
174
~Playlist()175 Playlist::~Playlist() {
176 items_.clear();
177 collection_items_by_id_.clear();
178 }
179
180 template <typename T>
InsertSongItems(const SongList & songs,const int pos,const bool play_now,const bool enqueue,const bool enqueue_next)181 void Playlist::InsertSongItems(const SongList &songs, const int pos, const bool play_now, const bool enqueue, const bool enqueue_next) {
182
183 PlaylistItemList items;
184 items.reserve(songs.count());
185 for (const Song &song : songs) {
186 items << std::make_shared<T>(song);
187 }
188
189 InsertItems(items, pos, play_now, enqueue, enqueue_next);
190
191 }
192
headerData(int section,Qt::Orientation,int role) const193 QVariant Playlist::headerData(int section, Qt::Orientation, int role) const {
194
195 if (role != Qt::DisplayRole && role != Qt::ToolTipRole) return QVariant();
196
197 const QString name = column_name(static_cast<Playlist::Column>(section));
198 if (!name.isEmpty()) return name;
199
200 return QVariant();
201
202 }
203
column_is_editable(Playlist::Column column)204 bool Playlist::column_is_editable(Playlist::Column column) {
205
206 switch (column) {
207 case Column_Title:
208 case Column_Artist:
209 case Column_Album:
210 case Column_AlbumArtist:
211 case Column_Composer:
212 case Column_Performer:
213 case Column_Grouping:
214 case Column_Track:
215 case Column_Disc:
216 case Column_Year:
217 case Column_Genre:
218 case Column_Comment:
219 return true;
220 default:
221 break;
222 }
223 return false;
224
225 }
226
set_column_value(Song & song,Playlist::Column column,const QVariant & value)227 bool Playlist::set_column_value(Song &song, Playlist::Column column, const QVariant &value) {
228
229 if (!song.IsEditable()) return false;
230
231 switch (column) {
232 case Column_Title:
233 song.set_title(value.toString());
234 break;
235 case Column_Artist:
236 song.set_artist(value.toString());
237 break;
238 case Column_Album:
239 song.set_album(value.toString());
240 break;
241 case Column_AlbumArtist:
242 song.set_albumartist(value.toString());
243 break;
244 case Column_Composer:
245 song.set_composer(value.toString());
246 break;
247 case Column_Performer:
248 song.set_performer(value.toString());
249 break;
250 case Column_Grouping:
251 song.set_grouping(value.toString());
252 break;
253 case Column_Track:
254 song.set_track(value.toInt());
255 break;
256 case Column_Disc:
257 song.set_disc(value.toInt());
258 break;
259 case Column_Year:
260 song.set_year(value.toInt());
261 break;
262 case Column_Genre:
263 song.set_genre(value.toString());
264 break;
265 case Column_Comment:
266 song.set_comment(value.toString());
267 break;
268 default:
269 break;
270 }
271
272 return true;
273
274 }
275
data(const QModelIndex & idx,int role) const276 QVariant Playlist::data(const QModelIndex &idx, int role) const {
277
278 switch (role) {
279 case Role_IsCurrent:
280 return current_item_index_.isValid() && idx.row() == current_item_index_.row();
281
282 case Role_IsPaused:
283 return current_is_paused_;
284
285 case Role_StopAfter:
286 return stop_after_.isValid() && stop_after_.row() == idx.row();
287
288 case Role_QueuePosition:
289 return queue_->PositionOf(idx);
290
291 case Role_CanSetRating:
292 return idx.column() == Column_Rating && items_[idx.row()]->IsLocalCollectionItem() && items_[idx.row()]->Metadata().id() != -1;
293
294 case Qt::EditRole:
295 case Qt::ToolTipRole:
296 case Qt::DisplayRole: {
297 PlaylistItemPtr item = items_[idx.row()];
298 Song song = item->Metadata();
299
300 // Don't forget to change Playlist::CompareItems when adding new columns
301 switch (idx.column()) {
302 case Column_Title: return song.PrettyTitle();
303 case Column_Artist: return song.artist();
304 case Column_Album: return song.album();
305 case Column_Length: return song.length_nanosec();
306 case Column_Track: return song.track();
307 case Column_Disc: return song.disc();
308 case Column_Year: return song.year();
309 case Column_OriginalYear: return song.effective_originalyear();
310 case Column_Genre: return song.genre();
311 case Column_AlbumArtist: return song.playlist_albumartist();
312 case Column_Composer: return song.composer();
313 case Column_Performer: return song.performer();
314 case Column_Grouping: return song.grouping();
315
316 case Column_PlayCount: return song.playcount();
317 case Column_SkipCount: return song.skipcount();
318 case Column_LastPlayed: return song.lastplayed();
319
320 case Column_Samplerate: return song.samplerate();
321 case Column_Bitdepth: return song.bitdepth();
322 case Column_Bitrate: return song.bitrate();
323
324 case Column_Filename: return song.effective_stream_url();
325 case Column_BaseFilename: return song.basefilename();
326 case Column_Filesize: return song.filesize();
327 case Column_Filetype: return song.filetype();
328 case Column_DateModified: return song.mtime();
329 case Column_DateCreated: return song.ctime();
330
331 case Column_Comment:
332 if (role == Qt::DisplayRole) return song.comment().simplified();
333 return song.comment();
334
335 case Column_Source: return song.source();
336
337 case Column_Rating: return song.rating();
338
339 }
340
341 return QVariant();
342 }
343
344 case Qt::TextAlignmentRole:
345 return QVariant(column_alignments_.value(idx.column(), (Qt::AlignLeft | Qt::AlignVCenter)));
346
347 case Qt::ForegroundRole:
348 if (data(idx, Role_IsCurrent).toBool()) {
349 // Ignore any custom colours for the currently playing item - they might clash with the glowing current track indicator.
350 return QVariant();
351 }
352
353 if (items_[idx.row()]->HasCurrentForegroundColor()) {
354 return QBrush(items_[idx.row()]->GetCurrentForegroundColor());
355 }
356 if (idx.row() < dynamic_history_length()) {
357 return QBrush(kDynamicHistoryColor);
358 }
359
360 return QVariant();
361
362 case Qt::BackgroundRole:
363 if (data(idx, Role_IsCurrent).toBool()) {
364 // Ignore any custom colours for the currently playing item - they might clash with the glowing current track indicator.
365 return QVariant();
366 }
367
368 if (items_[idx.row()]->HasCurrentBackgroundColor()) {
369 return QBrush(items_[idx.row()]->GetCurrentBackgroundColor());
370 }
371 return QVariant();
372
373 case Qt::FontRole:
374 if (items_[idx.row()]->GetShouldSkip()) {
375 QFont track_font;
376 track_font.setStrikeOut(true);
377 return track_font;
378 }
379 return QVariant();
380
381 default:
382 return QVariant();
383 }
384
385 }
386
387 #ifdef HAVE_MOODBAR
MoodbarUpdated(const QModelIndex & idx)388 void Playlist::MoodbarUpdated(const QModelIndex &idx) {
389 emit dataChanged(idx.sibling(idx.row(), Column_Mood), idx.sibling(idx.row(), Column_Mood));
390 }
391 #endif
392
setData(const QModelIndex & idx,const QVariant & value,int role)393 bool Playlist::setData(const QModelIndex &idx, const QVariant &value, int role) {
394
395 Q_UNUSED(role);
396
397 int row = idx.row();
398 PlaylistItemPtr item = item_at(row);
399 Song song = item->OriginalMetadata();
400
401 if (idx.data() == value) return false;
402
403 if (!set_column_value(song, static_cast<Column>(idx.column()), value)) return false;
404
405 if (song.url().isLocalFile()) {
406 TagReaderReply *reply = TagReaderClient::Instance()->SaveFile(song.url().toLocalFile(), song);
407 QPersistentModelIndex persistent_index = QPersistentModelIndex(idx);
408 QObject::connect(reply, &TagReaderReply::Finished, this, [this, reply, persistent_index, item]() { SongSaveComplete(reply, persistent_index, item->OriginalMetadata()); }, Qt::QueuedConnection);
409 }
410 else if (song.source() == Song::Source_Stream) {
411 item->SetMetadata(song);
412 ScheduleSave();
413 }
414
415 return true;
416
417 }
418
SongSaveComplete(TagReaderReply * reply,const QPersistentModelIndex & idx,const Song & old_metadata)419 void Playlist::SongSaveComplete(TagReaderReply *reply, const QPersistentModelIndex &idx, const Song &old_metadata) {
420
421 if (reply->is_successful() && idx.isValid()) {
422 if (reply->message().save_file_response().success()) {
423 ItemReload(idx, old_metadata, true);
424 }
425 else {
426 emit Error(tr("An error occurred writing metadata to '%1'").arg(QString::fromStdString(reply->request_message().save_file_request().filename())));
427 }
428 }
429
430 QMetaObject::invokeMethod(reply, "deleteLater", Qt::QueuedConnection);
431
432 }
433
ItemReload(const QPersistentModelIndex & idx,const Song & old_metadata,const bool metadata_edit)434 void Playlist::ItemReload(const QPersistentModelIndex &idx, const Song &old_metadata, const bool metadata_edit) {
435
436 if (idx.isValid()) {
437 PlaylistItemPtr item = item_at(idx.row());
438 if (item) {
439 QFuture<void> future = item->BackgroundReload();
440 QFutureWatcher<void> *watcher = new QFutureWatcher<void>();
441 QObject::connect(watcher, &QFutureWatcher<void>::finished, this, [this, watcher, idx, old_metadata, metadata_edit]() {
442 ItemReloadComplete(idx, old_metadata, metadata_edit);
443 watcher->deleteLater();
444 });
445 watcher->setFuture(future);
446 }
447 }
448
449 }
450
ItemReloadComplete(const QPersistentModelIndex & idx,const Song & old_metadata,const bool metadata_edit)451 void Playlist::ItemReloadComplete(const QPersistentModelIndex &idx, const Song &old_metadata, const bool metadata_edit) {
452
453 if (idx.isValid()) {
454 PlaylistItemPtr item = item_at(idx.row());
455 if (item) {
456 if (idx.row() == current_row()) {
457 const bool minor = old_metadata.title() == item->Metadata().title() &&
458 old_metadata.albumartist() == item->Metadata().albumartist() &&
459 old_metadata.artist() == item->Metadata().artist() &&
460 old_metadata.album() == item->Metadata().album();
461 InformOfCurrentSongChange(AutoScroll_Never, minor);
462 }
463 else {
464 emit dataChanged(index(idx.row(), 0), index(idx.row(), ColumnCount - 1));
465 }
466 if (metadata_edit) {
467 emit EditingFinished(id_, idx);
468 }
469 ScheduleSaveAsync();
470 }
471 }
472
473 }
474
current_row() const475 int Playlist::current_row() const {
476 return current_item_index_.isValid() ? current_item_index_.row() : -1;
477 }
478
current_index() const479 const QModelIndex Playlist::current_index() const {
480 return current_item_index_;
481 }
482
last_played_row() const483 int Playlist::last_played_row() const {
484 return last_played_item_index_.isValid() ? last_played_item_index_.row() : -1;
485 }
486
ShuffleModeChanged(const PlaylistSequence::ShuffleMode mode)487 void Playlist::ShuffleModeChanged(const PlaylistSequence::ShuffleMode mode) {
488 is_shuffled_ = (mode != PlaylistSequence::Shuffle_Off);
489 ReshuffleIndices();
490 }
491
FilterContainsVirtualIndex(const int i) const492 bool Playlist::FilterContainsVirtualIndex(const int i) const {
493 if (i < 0 || i >= virtual_items_.count()) return false;
494
495 return proxy_->filterAcceptsRow(virtual_items_[i], QModelIndex());
496 }
497
NextVirtualIndex(int i,const bool ignore_repeat_track) const498 int Playlist::NextVirtualIndex(int i, const bool ignore_repeat_track) const {
499
500 PlaylistSequence::RepeatMode repeat_mode = playlist_sequence_->repeat_mode();
501 PlaylistSequence::ShuffleMode shuffle_mode = playlist_sequence_->shuffle_mode();
502 bool album_only = repeat_mode == PlaylistSequence::Repeat_Album || shuffle_mode == PlaylistSequence::Shuffle_InsideAlbum;
503
504 // This one's easy - if we have to repeat the current track then just return i
505 if (repeat_mode == PlaylistSequence::Repeat_Track && !ignore_repeat_track) {
506 if (!FilterContainsVirtualIndex(i)) {
507 return virtual_items_.count(); // It's not in the filter any more
508 }
509 return i;
510 }
511
512 // If we're not bothered about whether a song is on the same album then return the next virtual index, whatever it is.
513 if (!album_only) {
514 ++i;
515
516 // Advance i until we find any track that is in the filter, skipping the selected to be skipped
517 while (i < virtual_items_.count() && (!FilterContainsVirtualIndex(i) || item_at(virtual_items_[i])->GetShouldSkip())) {
518 ++i;
519 }
520 return i;
521 }
522
523 // We need to advance i until we get something else on the same album
524 Song last_song = current_item_metadata();
525 for (int j = i + 1; j < virtual_items_.count(); ++j) {
526 if (item_at(virtual_items_[j])->GetShouldSkip()) {
527 continue;
528 }
529 Song this_song = item_at(virtual_items_[j])->Metadata();
530 if (((last_song.is_compilation() && this_song.is_compilation()) ||
531 last_song.effective_albumartist() == this_song.effective_albumartist()) &&
532 last_song.album() == this_song.album() &&
533 FilterContainsVirtualIndex(j)) {
534 return j; // Found one
535 }
536 }
537
538 // Couldn't find one - return past the end of the list
539 return virtual_items_.count();
540
541 }
542
PreviousVirtualIndex(int i,const bool ignore_repeat_track) const543 int Playlist::PreviousVirtualIndex(int i, const bool ignore_repeat_track) const {
544
545 PlaylistSequence::RepeatMode repeat_mode = playlist_sequence_->repeat_mode();
546 PlaylistSequence::ShuffleMode shuffle_mode = playlist_sequence_->shuffle_mode();
547 bool album_only = repeat_mode == PlaylistSequence::Repeat_Album || shuffle_mode == PlaylistSequence::Shuffle_InsideAlbum;
548
549 // This one's easy - if we have to repeat the current track then just return i
550 if (repeat_mode == PlaylistSequence::Repeat_Track && !ignore_repeat_track) {
551 if (!FilterContainsVirtualIndex(i)) return -1;
552 return i;
553 }
554
555 // If we're not bothered about whether a song is on the same album then return the previous virtual index, whatever it is.
556 if (!album_only) {
557 --i;
558
559 // Decrement i until we find any track that is in the filter
560 while (i >= 0 && (!FilterContainsVirtualIndex(i) || item_at(virtual_items_[i])->GetShouldSkip())) --i;
561 return i;
562 }
563
564 // We need to decrement i until we get something else on the same album
565 Song last_song = current_item_metadata();
566 for (int j = i - 1; j >= 0; --j) {
567 if (item_at(virtual_items_[j])->GetShouldSkip()) {
568 continue;
569 }
570 Song this_song = item_at(virtual_items_[j])->Metadata();
571 if (((last_song.is_compilation() && this_song.is_compilation()) || last_song.artist() == this_song.artist()) && last_song.album() == this_song.album() && FilterContainsVirtualIndex(j)) {
572 return j; // Found one
573 }
574 }
575
576 // Couldn't find one - return before the start of the list
577 return -1;
578
579 }
580
next_row(const bool ignore_repeat_track) const581 int Playlist::next_row(const bool ignore_repeat_track) const {
582
583 // Any queued items take priority
584 if (!queue_->is_empty()) {
585 return queue_->PeekNext();
586 }
587
588 int next_virtual_index = NextVirtualIndex(current_virtual_index_, ignore_repeat_track);
589 if (next_virtual_index >= virtual_items_.count()) {
590 // We've gone off the end of the playlist.
591
592 switch (playlist_sequence_->repeat_mode()) {
593 case PlaylistSequence::Repeat_Off:
594 case PlaylistSequence::Repeat_Intro:
595 return -1;
596 case PlaylistSequence::Repeat_Track:
597 next_virtual_index = current_virtual_index_;
598 break;
599
600 default:
601 next_virtual_index = NextVirtualIndex(-1, ignore_repeat_track);
602 break;
603 }
604 }
605
606 // Still off the end? Then just give up
607 if (next_virtual_index < 0 || next_virtual_index >= virtual_items_.count()) return -1;
608
609 return virtual_items_[next_virtual_index];
610
611 }
612
previous_row(const bool ignore_repeat_track) const613 int Playlist::previous_row(const bool ignore_repeat_track) const {
614
615 int prev_virtual_index = PreviousVirtualIndex(current_virtual_index_, ignore_repeat_track);
616
617 if (prev_virtual_index < 0) {
618 // We've gone off the beginning of the playlist.
619
620 switch (playlist_sequence_->repeat_mode()) {
621 case PlaylistSequence::Repeat_Off:
622 return -1;
623 case PlaylistSequence::Repeat_Track:
624 prev_virtual_index = current_virtual_index_;
625 break;
626
627 default:
628 prev_virtual_index = PreviousVirtualIndex(virtual_items_.count(), ignore_repeat_track);
629 break;
630 }
631 }
632
633 // Still off the beginning? Then just give up
634 if (prev_virtual_index < 0) return -1;
635
636 return virtual_items_[prev_virtual_index];
637
638 }
639
set_current_row(const int i,const AutoScroll autoscroll,const bool is_stopping,const bool force_inform)640 void Playlist::set_current_row(const int i, const AutoScroll autoscroll, const bool is_stopping, const bool force_inform) {
641
642 QModelIndex old_current_item_index = current_item_index_;
643 QModelIndex new_current_item_index;
644 if (i != -1) new_current_item_index = QPersistentModelIndex(index(i, 0, QModelIndex()));
645
646 if (new_current_item_index != current_item_index_) ClearStreamMetadata();
647
648 int nextrow = next_row();
649 if (nextrow != -1 && nextrow != i) {
650 PlaylistItemPtr next_item = item_at(nextrow);
651 if (next_item) {
652 next_item->ClearTemporaryMetadata();
653 emit dataChanged(index(nextrow, 0), index(nextrow, ColumnCount - 1));
654 }
655 }
656
657 current_item_index_ = new_current_item_index;
658
659 // if the given item is the first in the queue, remove it from the queue
660 if (current_item_index_.isValid() && current_item_index_.row() == queue_->PeekNext()) {
661 queue_->TakeNext();
662 }
663
664 if (current_item_index_ == old_current_item_index && !force_inform) {
665 UpdateScrobblePoint();
666 return;
667 }
668
669 if (old_current_item_index.isValid()) {
670 emit dataChanged(old_current_item_index, old_current_item_index.sibling(old_current_item_index.row(), ColumnCount - 1));
671 }
672
673 // Update the virtual index
674 if (i == -1) {
675 current_virtual_index_ = -1;
676 }
677 else if (is_shuffled_ && current_virtual_index_ == -1) {
678 // This is the first thing we're playing so we want to make sure the array is shuffled
679 ReshuffleIndices();
680
681 // Bring the one we've been asked to play to the start of the list
682 virtual_items_.takeAt(virtual_items_.indexOf(i));
683 virtual_items_.prepend(i);
684 current_virtual_index_ = 0;
685 }
686 else if (is_shuffled_) {
687 current_virtual_index_ = virtual_items_.indexOf(i);
688 }
689 else {
690 current_virtual_index_ = i;
691 }
692
693 if (current_item_index_.isValid() && !is_stopping) {
694 InformOfCurrentSongChange(autoscroll, false);
695 }
696
697 // The structure of a dynamic playlist is as follows:
698 // history - active song - future
699 // We have to ensure that this invariant is maintained.
700 if (dynamic_playlist_ && current_item_index_.isValid()) {
701
702 // When advancing to the next track
703 if (old_current_item_index.isValid() && i > old_current_item_index.row()) {
704 // Move the new item one position ahead of the last item in the history.
705 MoveItemWithoutUndo(current_item_index_.row(), dynamic_history_length());
706
707 // Compute the number of new items that have to be inserted
708 // This is not necessarily 1 because the user might have added or removed items manually.
709 // Note that the future excludes the current item.
710 const int count = static_cast<int>(dynamic_history_length() + 1 + dynamic_playlist_->GetDynamicFuture() - items_.count());
711 if (count > 0) {
712 InsertDynamicItems(count);
713 }
714
715 // Shrink the history, again this is not necessarily by 1, because the user might have moved items by hand.
716 const int remove_count = dynamic_history_length() - dynamic_playlist_->GetDynamicHistory();
717 if (0 < remove_count) RemoveItemsWithoutUndo(0, remove_count);
718 }
719
720 // the above actions make all commands on the undo stack invalid, so we better clear it.
721 undo_stack_->clear();
722 }
723
724 if (current_item_index_.isValid()) {
725 last_played_item_index_ = current_item_index_;
726 ScheduleSave();
727 }
728
729 UpdateScrobblePoint();
730
731 }
732
InsertDynamicItems(const int count)733 void Playlist::InsertDynamicItems(const int count) {
734
735 PlaylistGeneratorInserter *inserter = new PlaylistGeneratorInserter(task_manager_, collection_, this);
736 QObject::connect(inserter, &PlaylistGeneratorInserter::Error, this, &Playlist::Error);
737 QObject::connect(inserter, &PlaylistGeneratorInserter::PlayRequested, this, &Playlist::PlayRequested);
738
739 inserter->Load(this, -1, false, false, false, dynamic_playlist_, count);
740
741 }
742
flags(const QModelIndex & idx) const743 Qt::ItemFlags Playlist::flags(const QModelIndex &idx) const {
744
745 if (idx.isValid()) {
746 Qt::ItemFlags flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled;
747 if (item_at(idx.row())->Metadata().IsEditable() && column_is_editable(static_cast<Column>(idx.column()))) flags |= Qt::ItemIsEditable;
748 return flags;
749 }
750 else {
751 return Qt::ItemIsDropEnabled;
752 }
753
754 }
755
mimeTypes() const756 QStringList Playlist::mimeTypes() const {
757
758 return QStringList() << "text/uri-list" << kRowsMimetype;
759
760 }
761
supportedDropActions() const762 Qt::DropActions Playlist::supportedDropActions() const {
763 return Qt::MoveAction | Qt::CopyAction | Qt::LinkAction;
764 }
765
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int,const QModelIndex &)766 bool Playlist::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int, const QModelIndex&) {
767
768 if (action == Qt::IgnoreAction) return false;
769
770 bool play_now = false;
771 bool enqueue_now = false;
772 bool enqueue_next_now = false;
773
774 if (const MimeData *mime_data = qobject_cast<const MimeData*>(data)) {
775 if (mime_data->clear_first_) {
776 Clear();
777 }
778 play_now = mime_data->play_now_;
779 enqueue_now = mime_data->enqueue_now_;
780 enqueue_next_now = mime_data->enqueue_next_now_;
781 }
782
783 if (const SongMimeData *song_data = qobject_cast<const SongMimeData*>(data)) {
784 // Dragged from a collection
785 // We want to check if these songs are from the actual local file backend, if they are we treat them differently.
786 if (song_data->backend && song_data->backend->songs_table() == SCollection::kSongsTable) {
787 InsertSongItems<CollectionPlaylistItem>(song_data->songs, row, play_now, enqueue_now, enqueue_next_now);
788 }
789 else {
790 InsertSongItems<SongPlaylistItem>(song_data->songs, row, play_now, enqueue_now, enqueue_next_now);
791 }
792 }
793 else if (const PlaylistItemMimeData *item_data = qobject_cast<const PlaylistItemMimeData*>(data)) {
794 InsertItems(item_data->items_, row, play_now, enqueue_now, enqueue_next_now);
795 }
796 else if (const PlaylistGeneratorMimeData *generator_data = qobject_cast<const PlaylistGeneratorMimeData*>(data)) {
797 InsertSmartPlaylist(generator_data->generator_, row, play_now, enqueue_now, enqueue_next_now);
798 }
799 else if (const InternetSongMimeData *internet_song_data = qobject_cast<const InternetSongMimeData*>(data)) {
800 InsertInternetItems(internet_song_data->service, internet_song_data->songs.values(), row, play_now, enqueue_now, enqueue_next_now);
801 }
802 else if (const RadioMimeData *radio_data = qobject_cast<const RadioMimeData*>(data)) {
803 InsertRadioItems(radio_data->songs, row, play_now, enqueue_now, enqueue_next_now);
804 }
805 else if (data->hasFormat(kRowsMimetype)) {
806 // Dragged from the playlist
807 // Rearranging it is tricky...
808
809 // Get the list of rows that were moved
810 QList<int> source_rows;
811 Playlist *source_playlist = nullptr;
812 qint64 pid = 0;
813 qint64 own_pid = QCoreApplication::applicationPid();
814
815 QDataStream stream(data->data(kRowsMimetype));
816 stream.readRawData(reinterpret_cast<char*>(&source_playlist), sizeof(source_playlist)); // NOLINT(bugprone-sizeof-expression)
817 stream >> source_rows;
818 if (!stream.atEnd()) {
819 stream.readRawData(reinterpret_cast<char*>(&pid), sizeof(pid));
820 }
821 else {
822 pid = own_pid;
823 }
824
825 std::stable_sort(source_rows.begin(), source_rows.end()); // Make sure we take them in order
826
827 if (source_playlist == this) {
828 // Dragged from this playlist - rearrange the items
829 undo_stack_->push(new PlaylistUndoCommands::MoveItems(this, source_rows, row));
830 }
831 else if (pid == own_pid) {
832 // Drag from a different playlist
833 PlaylistItemList items;
834 items.reserve(source_rows.count());
835 for (const int i : source_rows) items << source_playlist->item_at(i);
836
837 if (items.count() > kUndoItemLimit) {
838 // Too big to keep in the undo stack. Also clear the stack because it might have been invalidated.
839 InsertItemsWithoutUndo(items, row, false, false);
840 undo_stack_->clear();
841 }
842 else {
843 undo_stack_->push(new PlaylistUndoCommands::InsertItems(this, items, row));
844 }
845
846 // Remove the items from the source playlist if it was a move event
847 if (action == Qt::MoveAction) {
848 for (const int i : source_rows) {
849 source_playlist->undo_stack()->push(new PlaylistUndoCommands::RemoveItems(source_playlist, i, 1));
850 }
851 }
852 }
853 }
854 else if (data->hasFormat(kCddaMimeType)) {
855 SongLoaderInserter *inserter = new SongLoaderInserter(task_manager_, collection_, backend_->app()->player());
856 QObject::connect(inserter, &SongLoaderInserter::Error, this, &Playlist::Error);
857 inserter->LoadAudioCD(this, row, play_now, enqueue_now, enqueue_next_now);
858 }
859 else if (data->hasUrls()) {
860 // URL list dragged from the file list or some other app
861 InsertUrls(data->urls(), row, play_now, enqueue_now, enqueue_next_now);
862 }
863
864 return true;
865
866 }
867
InsertUrls(const QList<QUrl> & urls,const int pos,const bool play_now,const bool enqueue,const bool enqueue_next)868 void Playlist::InsertUrls(const QList<QUrl> &urls, const int pos, const bool play_now, const bool enqueue, const bool enqueue_next) {
869
870 SongLoaderInserter *inserter = new SongLoaderInserter(task_manager_, collection_, backend_->app()->player());
871 QObject::connect(inserter, &SongLoaderInserter::Error, this, &Playlist::Error);
872
873 inserter->Load(this, pos, play_now, enqueue, enqueue_next, urls);
874
875 }
876
InsertSmartPlaylist(PlaylistGeneratorPtr generator,const int pos,const bool play_now,const bool enqueue,const bool enqueue_next)877 void Playlist::InsertSmartPlaylist(PlaylistGeneratorPtr generator, const int pos, const bool play_now, const bool enqueue, const bool enqueue_next) {
878
879 // Hack: If the generator hasn't got a collection set then use the main one
880 if (!generator->collection()) {
881 generator->set_collection(collection_);
882 }
883
884 PlaylistGeneratorInserter *inserter = new PlaylistGeneratorInserter(task_manager_, collection_, this);
885 QObject::connect(inserter, &PlaylistGeneratorInserter::Error, this, &Playlist::Error);
886
887 inserter->Load(this, pos, play_now, enqueue, enqueue_next, generator);
888
889 if (generator->is_dynamic()) {
890 TurnOnDynamicPlaylist(generator);
891 }
892
893 }
894
TurnOnDynamicPlaylist(PlaylistGeneratorPtr gen)895 void Playlist::TurnOnDynamicPlaylist(PlaylistGeneratorPtr gen) {
896
897 dynamic_playlist_ = gen;
898 ShuffleModeChanged(PlaylistSequence::Shuffle_Off);
899 emit DynamicModeChanged(true);
900
901 ScheduleSave();
902
903 }
904
MoveItemWithoutUndo(const int source,const int dest)905 void Playlist::MoveItemWithoutUndo(const int source, const int dest) {
906 MoveItemsWithoutUndo(QList<int>() << source, dest);
907 }
908
MoveItemsWithoutUndo(const QList<int> & source_rows,int pos)909 void Playlist::MoveItemsWithoutUndo(const QList<int> &source_rows, int pos) {
910
911 emit layoutAboutToBeChanged();
912 PlaylistItemList moved_items;
913 moved_items.reserve(source_rows.count());
914
915 if (pos < 0) {
916 pos = items_.count();
917 }
918
919 // Take the items out of the list first, keeping track of whether the insertion point changes
920 int offset = 0;
921 int start = pos;
922 for (const int source_row : source_rows) {
923 moved_items << items_.takeAt(source_row - offset);
924 if (pos > source_row) {
925 --start;
926 }
927 ++offset;
928 }
929
930 // Put the items back in
931 for (int i = start; i < start + moved_items.count(); ++i) {
932 moved_items[i - start]->RemoveForegroundColor(kDynamicHistoryPriority);
933 items_.insert(i, moved_items[i - start]);
934 }
935
936 // Update persistent indexes
937 for (const QModelIndex &pidx : persistentIndexList()) {
938 const int dest_offset = source_rows.indexOf(pidx.row());
939 if (dest_offset != -1) {
940 // This index was moved
941 changePersistentIndex(pidx, index(start + dest_offset, pidx.column(), QModelIndex()));
942 }
943 else {
944 int d = 0;
945 for (int source_row : source_rows) {
946 if (pidx.row() > source_row) d--;
947 }
948 if (pidx.row() + d >= start) d += source_rows.count();
949
950 changePersistentIndex(pidx, index(pidx.row() + d, pidx.column(), QModelIndex()));
951 }
952 }
953 current_virtual_index_ = virtual_items_.indexOf(current_row());
954
955 emit layoutChanged();
956
957 ScheduleSave();
958
959 }
960
MoveItemsWithoutUndo(int start,const QList<int> & dest_rows)961 void Playlist::MoveItemsWithoutUndo(int start, const QList<int> &dest_rows) {
962
963 emit layoutAboutToBeChanged();
964
965 PlaylistItemList moved_items;
966 moved_items.reserve(dest_rows.count());
967
968 int pos = start;
969 for (const int dest_row : dest_rows) {
970 if (dest_row < pos) --start;
971 }
972
973 if (start < 0) {
974 start = static_cast<int>(items_.count() - dest_rows.count());
975 }
976
977 // Take the items out of the list first
978 for (int i = 0; i < dest_rows.count(); ++i) {
979 moved_items << items_.takeAt(start);
980 }
981
982 // Put the items back in
983 int offset = 0;
984 for (int dest_row : dest_rows) {
985 items_.insert(dest_row, moved_items[offset]);
986 offset++;
987 }
988
989 // Update persistent indexes
990 for (const QModelIndex &pidx : persistentIndexList()) {
991 if (pidx.row() >= start && pidx.row() < start + dest_rows.count()) {
992 // This index was moved
993 const int i = pidx.row() - start;
994 changePersistentIndex(pidx, index(dest_rows[i], pidx.column(), QModelIndex()));
995 }
996 else {
997 int d = 0;
998 if (pidx.row() >= start + dest_rows.count()) {
999 d -= dest_rows.count();
1000 }
1001
1002 for (int dest_row : dest_rows) {
1003 if (pidx.row() + d > dest_row) d++;
1004 }
1005
1006 changePersistentIndex(pidx, index(pidx.row() + d, pidx.column(), QModelIndex()));
1007 }
1008 }
1009 current_virtual_index_ = virtual_items_.indexOf(current_row());
1010
1011 emit layoutChanged();
1012
1013 ScheduleSave();
1014
1015 }
1016
InsertItems(const PlaylistItemList & itemsIn,const int pos,const bool play_now,const bool enqueue,const bool enqueue_next)1017 void Playlist::InsertItems(const PlaylistItemList &itemsIn, const int pos, const bool play_now, const bool enqueue, const bool enqueue_next) {
1018
1019 if (itemsIn.isEmpty()) {
1020 return;
1021 }
1022
1023 PlaylistItemList items = itemsIn;
1024
1025 // Exercise vetoes
1026 SongList songs;
1027 songs.reserve(items.count());
1028 for (PlaylistItemPtr item : items) { // clazy:exclude=range-loop-reference
1029 songs << item->Metadata();
1030 }
1031
1032 const int song_count = songs.length();
1033 QSet<Song> vetoed;
1034 for (SongInsertVetoListener *listener : veto_listeners_) {
1035 for (const Song &song : listener->AboutToInsertSongs(GetAllSongs(), songs)) {
1036 // Avoid veto-ing a song multiple times
1037 vetoed.insert(song);
1038 }
1039 if (vetoed.count() == song_count) {
1040 // All songs were vetoed and there's nothing more to do (there's no need for an undo step)
1041 return;
1042 }
1043 }
1044
1045 if (!vetoed.isEmpty()) {
1046 QMutableListIterator<PlaylistItemPtr> it(items);
1047 while (it.hasNext()) {
1048 PlaylistItemPtr item = it.next();
1049 const Song ¤t = item->Metadata();
1050
1051 if (vetoed.contains(current)) {
1052 vetoed.remove(current);
1053 it.remove();
1054 }
1055 }
1056
1057 // Check for empty items once again after veto
1058 if (items.isEmpty()) {
1059 return;
1060 }
1061 }
1062
1063 const int start = pos == -1 ? static_cast<int>(items_.count()) : pos;
1064
1065 if (items.count() > kUndoItemLimit) {
1066 // Too big to keep in the undo stack. Also clear the stack because it might have been invalidated.
1067 InsertItemsWithoutUndo(items, pos, enqueue, enqueue_next);
1068 undo_stack_->clear();
1069 }
1070 else {
1071 undo_stack_->push(new PlaylistUndoCommands::InsertItems(this, items, pos, enqueue, enqueue_next));
1072 }
1073
1074 if (play_now) emit PlayRequested(index(start, 0), AutoScroll_Maybe);
1075
1076 }
1077
InsertItemsWithoutUndo(const PlaylistItemList & items,const int pos,const bool enqueue,const bool enqueue_next)1078 void Playlist::InsertItemsWithoutUndo(const PlaylistItemList &items, const int pos, const bool enqueue, const bool enqueue_next) {
1079
1080 if (items.isEmpty()) return;
1081
1082 const int start = pos == -1 ? static_cast<int>(items_.count()) : pos;
1083 const int end = start + static_cast<int>(items.count()) - 1;
1084
1085 beginInsertRows(QModelIndex(), start, end);
1086 for (int i = start; i <= end; ++i) {
1087 PlaylistItemPtr item = items[i - start];
1088 items_.insert(i, item);
1089 virtual_items_ << virtual_items_.count();
1090
1091 if (item->source() == Song::Source_Collection) {
1092 int id = item->Metadata().id();
1093 if (id != -1) {
1094 collection_items_by_id_.insert(id, item);
1095 }
1096 }
1097
1098 if (item == current_item()) {
1099 // It's one we removed before that got re-added through an undo
1100 current_item_index_ = index(i, 0);
1101 last_played_item_index_ = current_item_index_;
1102 }
1103 }
1104 endInsertRows();
1105
1106 if (enqueue) {
1107 QModelIndexList indexes;
1108 for (int i = start; i <= end; ++i) {
1109 indexes << index(i, 0); // clazy:exclude=reserve-candidates
1110 }
1111 queue_->ToggleTracks(indexes);
1112 }
1113
1114 if (enqueue_next) {
1115 QModelIndexList indexes;
1116 for (int i = start; i <= end; ++i) {
1117 indexes << index(i, 0); // clazy:exclude=reserve-candidates
1118 }
1119 queue_->InsertFirst(indexes);
1120 }
1121
1122 ScheduleSave();
1123
1124 if (auto_sort_) {
1125 sort(sort_column_, sort_order_);
1126 }
1127 else {
1128 ReshuffleIndices();
1129 }
1130
1131 }
1132
InsertCollectionItems(const SongList & songs,const int pos,const bool play_now,const bool enqueue,const bool enqueue_next)1133 void Playlist::InsertCollectionItems(const SongList &songs, const int pos, const bool play_now, const bool enqueue, const bool enqueue_next) {
1134 InsertSongItems<CollectionPlaylistItem>(songs, pos, play_now, enqueue, enqueue_next);
1135 }
1136
InsertSongs(const SongList & songs,const int pos,const bool play_now,const bool enqueue,const bool enqueue_next)1137 void Playlist::InsertSongs(const SongList &songs, const int pos, const bool play_now, const bool enqueue, const bool enqueue_next) {
1138 InsertSongItems<SongPlaylistItem>(songs, pos, play_now, enqueue, enqueue_next);
1139 }
1140
InsertSongsOrCollectionItems(const SongList & songs,const int pos,const bool play_now,const bool enqueue,const bool enqueue_next)1141 void Playlist::InsertSongsOrCollectionItems(const SongList &songs, const int pos, const bool play_now, const bool enqueue, const bool enqueue_next) {
1142
1143 PlaylistItemList items;
1144 for (const Song &song : songs) {
1145 if (song.is_collection_song()) {
1146 items << std::make_shared<CollectionPlaylistItem>(song);
1147 }
1148 else {
1149 items << std::make_shared<SongPlaylistItem>(song);
1150 }
1151 }
1152 InsertItems(items, pos, play_now, enqueue, enqueue_next);
1153
1154 }
1155
InsertInternetItems(InternetService * service,const SongList & songs,const int pos,const bool play_now,const bool enqueue,const bool enqueue_next)1156 void Playlist::InsertInternetItems(InternetService *service, const SongList &songs, const int pos, const bool play_now, const bool enqueue, const bool enqueue_next) {
1157
1158 PlaylistItemList playlist_items;
1159 playlist_items.reserve(songs.count());
1160 for (const Song &song : songs) {
1161 playlist_items << std::make_shared<InternetPlaylistItem>(service, song);
1162 }
1163
1164 InsertItems(playlist_items, pos, play_now, enqueue, enqueue_next);
1165
1166 }
1167
InsertRadioItems(const SongList & songs,const int pos,const bool play_now,const bool enqueue,const bool enqueue_next)1168 void Playlist::InsertRadioItems(const SongList &songs, const int pos, const bool play_now, const bool enqueue, const bool enqueue_next) {
1169
1170 PlaylistItemList playlist_items;
1171 playlist_items.reserve(songs.count());
1172 for (const Song &song : songs) {
1173 playlist_items << std::make_shared<RadioPlaylistItem>(song);
1174 }
1175
1176 InsertItems(playlist_items, pos, play_now, enqueue, enqueue_next);
1177
1178 }
1179
UpdateItems(SongList songs)1180 void Playlist::UpdateItems(SongList songs) {
1181
1182 qLog(Debug) << "Updating playlist with new tracks' info";
1183
1184 // We first convert our songs list into a linked list (a 'real' list), because removals are faster with QLinkedList.
1185 // Next, we walk through the list of playlist's items then the list of songs
1186 // we want to update: if an item corresponds to the song (we rely on URL for this), we update the item with the new metadata,
1187 // then we remove song from our list because we will not need to check it again.
1188 // And we also update undo actions.
1189
1190 for (int i = 0; i < items_.size(); i++) {
1191 // Update current items list
1192 QMutableListIterator<Song> it(songs);
1193 while (it.hasNext()) {
1194 const Song &song = it.next();
1195 const PlaylistItemPtr &item = items_[i];
1196 if (item->Metadata().url() == song.url() && (item->Metadata().filetype() == Song::FileType_Unknown || item->Metadata().filetype() == Song::FileType_Stream || item->Metadata().filetype() == Song::FileType_CDDA || !item->Metadata().init_from_file())) {
1197 PlaylistItemPtr new_item;
1198 if (song.is_collection_song()) {
1199 new_item = std::make_shared<CollectionPlaylistItem>(song);
1200 if (collection_items_by_id_.contains(song.id(), item)) collection_items_by_id_.remove(song.id(), item);
1201 collection_items_by_id_.insert(song.id(), new_item);
1202 }
1203 else {
1204 new_item = std::make_shared<SongPlaylistItem>(song);
1205 }
1206 items_[i] = new_item;
1207 emit dataChanged(index(i, 0), index(i, ColumnCount - 1));
1208 // Also update undo actions
1209 for (int y = 0; y < undo_stack_->count(); y++) {
1210 QUndoCommand *undo_action = const_cast<QUndoCommand*>(undo_stack_->command(i));
1211 PlaylistUndoCommands::InsertItems *undo_action_insert = dynamic_cast<PlaylistUndoCommands::InsertItems*>(undo_action);
1212 if (undo_action_insert) {
1213 bool found_and_updated = undo_action_insert->UpdateItem(new_item);
1214 if (found_and_updated) break;
1215 }
1216 }
1217 it.remove();
1218 break;
1219 }
1220 }
1221 }
1222
1223 ScheduleSave();
1224
1225 }
1226
mimeData(const QModelIndexList & indexes) const1227 QMimeData *Playlist::mimeData(const QModelIndexList &indexes) const {
1228
1229 if (indexes.isEmpty()) return nullptr;
1230
1231 // We only want one index per row, but we can't just take column 0 because the user might have hidden it.
1232 const int first_column = indexes.first().column();
1233
1234 QMimeData *mimedata = new QMimeData;
1235
1236 QList<QUrl> urls;
1237 QList<int> rows;
1238 for (const QModelIndex &idx : indexes) {
1239 if (idx.column() != first_column) continue;
1240
1241 urls << items_[idx.row()]->Url();
1242 rows << idx.row();
1243 }
1244
1245 QBuffer buf;
1246 if (!buf.open(QIODevice::WriteOnly)) {
1247 delete mimedata;
1248 return nullptr;
1249 }
1250 QDataStream stream(&buf);
1251
1252 const Playlist *self = this;
1253 const qint64 pid = QCoreApplication::applicationPid();
1254
1255 stream.writeRawData(reinterpret_cast<char*>(&self), sizeof(self)); // NOLINT(bugprone-sizeof-expression)
1256 stream << rows;
1257 stream.writeRawData(reinterpret_cast<const char*>(&pid), sizeof(pid));
1258 buf.close();
1259
1260 mimedata->setUrls(urls);
1261 mimedata->setData(kRowsMimetype, buf.data());
1262
1263 return mimedata;
1264
1265 }
1266
CompareItems(const int column,const Qt::SortOrder order,std::shared_ptr<PlaylistItem> _a,std::shared_ptr<PlaylistItem> _b)1267 bool Playlist::CompareItems(const int column, const Qt::SortOrder order, std::shared_ptr<PlaylistItem> _a, std::shared_ptr<PlaylistItem> _b) {
1268
1269 std::shared_ptr<PlaylistItem> a = order == Qt::AscendingOrder ? _a : _b;
1270 std::shared_ptr<PlaylistItem> b = order == Qt::AscendingOrder ? _b : _a;
1271
1272 #define cmp(field) return a->Metadata().field() < b->Metadata().field()
1273 #define strcmp(field) return QString::localeAwareCompare(a->Metadata().field().toLower(), b->Metadata().field().toLower()) < 0;
1274
1275 switch (column) {
1276
1277 case Column_Title: strcmp(title_sortable);
1278 case Column_Artist: strcmp(artist_sortable);
1279 case Column_Album: strcmp(album_sortable);
1280 case Column_Length: cmp(length_nanosec);
1281 case Column_Track: cmp(track);
1282 case Column_Disc: cmp(disc);
1283 case Column_Year: cmp(year);
1284 case Column_OriginalYear: cmp(originalyear);
1285 case Column_Genre: strcmp(genre);
1286 case Column_AlbumArtist: strcmp(playlist_albumartist_sortable);
1287 case Column_Composer: strcmp(composer);
1288 case Column_Performer: strcmp(performer);
1289 case Column_Grouping: strcmp(grouping);
1290
1291 case Column_PlayCount: cmp(playcount);
1292 case Column_SkipCount: cmp(skipcount);
1293 case Column_LastPlayed: cmp(lastplayed);
1294
1295 case Column_Bitrate: cmp(bitrate);
1296 case Column_Samplerate: cmp(samplerate);
1297 case Column_Bitdepth: cmp(bitdepth);
1298 case Column_Filename:
1299 return (QString::localeAwareCompare(a->Url().path().toLower(), b->Url().path().toLower()) < 0);
1300 case Column_BaseFilename: cmp(basefilename);
1301 case Column_Filesize: cmp(filesize);
1302 case Column_Filetype: cmp(filetype);
1303 case Column_DateModified: cmp(mtime);
1304 case Column_DateCreated: cmp(ctime);
1305
1306 case Column_Comment: strcmp(comment);
1307 case Column_Source: cmp(source);
1308
1309 case Column_Rating: cmp(rating);
1310
1311 default: qLog(Error) << "No such column" << column;
1312 }
1313
1314 #undef cmp
1315 #undef strcmp
1316
1317 return false;
1318
1319 }
1320
ComparePathDepths(const Qt::SortOrder order,std::shared_ptr<PlaylistItem> _a,std::shared_ptr<PlaylistItem> _b)1321 bool Playlist::ComparePathDepths(const Qt::SortOrder order, std::shared_ptr<PlaylistItem> _a, std::shared_ptr<PlaylistItem> _b) {
1322
1323 std::shared_ptr<PlaylistItem> a = order == Qt::AscendingOrder ? _a : _b;
1324 std::shared_ptr<PlaylistItem> b = order == Qt::AscendingOrder ? _b : _a;
1325
1326 int a_dir_level = a->Url().path().count('/');
1327 int b_dir_level = b->Url().path().count('/');
1328
1329 return a_dir_level < b_dir_level;
1330
1331 }
1332
column_name(Column column)1333 QString Playlist::column_name(Column column) {
1334
1335 switch (column) {
1336 case Column_Title: return tr("Title");
1337 case Column_Artist: return tr("Artist");
1338 case Column_Album: return tr("Album");
1339 case Column_Track: return tr("Track");
1340 case Column_Disc: return tr("Disc");
1341 case Column_Length: return tr("Length");
1342 case Column_Year: return tr("Year");
1343 case Column_OriginalYear: return tr("Original year");
1344 case Column_Genre: return tr("Genre");
1345 case Column_AlbumArtist: return tr("Album artist");
1346 case Column_Composer: return tr("Composer");
1347 case Column_Performer: return tr("Performer");
1348 case Column_Grouping: return tr("Grouping");
1349
1350 case Column_PlayCount: return tr("Play count");
1351 case Column_SkipCount: return tr("Skip count");
1352 case Column_LastPlayed: return tr("Last played");
1353
1354 case Column_Samplerate: return tr("Sample rate");
1355 case Column_Bitdepth: return tr("Bit depth");
1356 case Column_Bitrate: return tr("Bitrate");
1357
1358 case Column_Filename: return tr("File name");
1359 case Column_BaseFilename: return tr("File name (without path)");
1360 case Column_Filesize: return tr("File size");
1361 case Column_Filetype: return tr("File type");
1362 case Column_DateModified: return tr("Date modified");
1363 case Column_DateCreated: return tr("Date created");
1364
1365 case Column_Comment: return tr("Comment");
1366 case Column_Source: return tr("Source");
1367 case Column_Mood: return tr("Mood");
1368 case Column_Rating: return tr("Rating");
1369 default: qLog(Error) << "No such column" << column;;
1370 }
1371 return "";
1372
1373 }
1374
abbreviated_column_name(const Column column)1375 QString Playlist::abbreviated_column_name(const Column column) {
1376
1377 const QString &column_name = Playlist::column_name(column);
1378
1379 switch (column) {
1380 case Column_Disc:
1381 case Column_PlayCount:
1382 case Column_SkipCount:
1383 case Column_Track:
1384 return QString("%1#").arg(column_name[0]);
1385 default:
1386 return column_name;
1387 }
1388 return "";
1389
1390 }
1391
sort(int column,Qt::SortOrder order)1392 void Playlist::sort(int column, Qt::SortOrder order) {
1393
1394 sort_column_ = column;
1395 sort_order_ = order;
1396
1397 if (ignore_sorting_) return;
1398
1399 PlaylistItemList new_items(items_);
1400 PlaylistItemList::iterator begin = new_items.begin();
1401
1402 if (dynamic_playlist_ && current_item_index_.isValid())
1403 begin += current_item_index_.row() + 1;
1404
1405 if (column == Column_Album) {
1406 // When sorting by album, also take into account discs and tracks.
1407 std::stable_sort(begin, new_items.end(), std::bind(&Playlist::CompareItems, Column_Track, order, std::placeholders::_1, std::placeholders::_2));
1408 std::stable_sort(begin, new_items.end(), std::bind(&Playlist::CompareItems, Column_Disc, order, std::placeholders::_1, std::placeholders::_2));
1409 std::stable_sort(begin, new_items.end(), std::bind(&Playlist::CompareItems, Column_Album, order, std::placeholders::_1, std::placeholders::_2));
1410 }
1411 else if (column == Column_Filename) {
1412 // When sorting by full paths we also expect a hierarchical order. This returns a breath-first ordering of paths.
1413 std::stable_sort(begin, new_items.end(), std::bind(&Playlist::CompareItems, Column_Filename, order, std::placeholders::_1, std::placeholders::_2));
1414 std::stable_sort(begin, new_items.end(), std::bind(&Playlist::ComparePathDepths, order, std::placeholders::_1, std::placeholders::_2));
1415 }
1416 else {
1417 std::stable_sort(begin, new_items.end(), std::bind(&Playlist::CompareItems, column, order, std::placeholders::_1, std::placeholders::_2));
1418 }
1419
1420 undo_stack_->push(new PlaylistUndoCommands::SortItems(this, column, order, new_items));
1421
1422 ReshuffleIndices();
1423
1424 }
1425
ReOrderWithoutUndo(const PlaylistItemList & new_items)1426 void Playlist::ReOrderWithoutUndo(const PlaylistItemList &new_items) {
1427
1428 emit layoutAboutToBeChanged();
1429
1430 PlaylistItemList old_items = items_;
1431 items_ = new_items;
1432
1433 QHash<const PlaylistItem*, int> new_rows;
1434 for (int i = 0; i < new_items.length(); ++i) {
1435 new_rows[new_items[i].get()] = i;
1436 }
1437
1438 for (const QModelIndex &idx : persistentIndexList()) {
1439 const PlaylistItem *item = old_items[idx.row()].get();
1440 changePersistentIndex(idx, index(new_rows[item], idx.column(), idx.parent()));
1441 }
1442
1443 emit layoutChanged();
1444
1445 emit PlaylistChanged();
1446
1447 ScheduleSave();
1448
1449 }
1450
Playing()1451 void Playlist::Playing() { SetCurrentIsPaused(false); }
1452
Paused()1453 void Playlist::Paused() { SetCurrentIsPaused(true); }
1454
Stopped()1455 void Playlist::Stopped() { SetCurrentIsPaused(false); }
1456
SetCurrentIsPaused(const bool paused)1457 void Playlist::SetCurrentIsPaused(const bool paused) {
1458
1459 if (paused == current_is_paused_) return;
1460
1461 current_is_paused_ = paused;
1462
1463 if (current_item_index_.isValid()) {
1464 emit dataChanged(index(current_item_index_.row(), 0), index(current_item_index_.row(), ColumnCount - 1));
1465 }
1466
1467 }
1468
ScheduleSaveAsync()1469 void Playlist::ScheduleSaveAsync() {
1470
1471 if (QThread::currentThread() == thread()) {
1472 ScheduleSave();
1473 }
1474 else {
1475 QMetaObject::invokeMethod(this, "ScheduleSave", Qt::QueuedConnection);
1476 }
1477
1478 }
1479
ScheduleSave()1480 void Playlist::ScheduleSave() {
1481
1482 if (!backend_ || is_loading_) return;
1483
1484 timer_save_->start();
1485
1486 }
1487
Save()1488 void Playlist::Save() {
1489
1490 if (!backend_ || is_loading_) return;
1491
1492 backend_->SavePlaylistAsync(id_, items_, last_played_row(), dynamic_playlist_);
1493
1494 }
1495
Restore()1496 void Playlist::Restore() {
1497
1498 if (!backend_) return;
1499
1500 items_.clear();
1501 virtual_items_.clear();
1502 collection_items_by_id_.clear();
1503
1504 cancel_restore_ = false;
1505 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
1506 QFuture<PlaylistItemList> future = QtConcurrent::run(&PlaylistBackend::GetPlaylistItems, backend_, id_);
1507 #else
1508 QFuture<PlaylistItemList> future = QtConcurrent::run(backend_, &PlaylistBackend::GetPlaylistItems, id_);
1509 #endif
1510 QFutureWatcher<PlaylistItemList> *watcher = new QFutureWatcher<PlaylistItemList>();
1511 QObject::connect(watcher, &QFutureWatcher<PlaylistItemList>::finished, this, &Playlist::ItemsLoaded);
1512 watcher->setFuture(future);
1513
1514 }
1515
ItemsLoaded()1516 void Playlist::ItemsLoaded() {
1517
1518 QFutureWatcher<PlaylistItemList> *watcher = static_cast<QFutureWatcher<PlaylistItemList>*>(sender());
1519 PlaylistItemList items = watcher->result();
1520 watcher->deleteLater();
1521
1522 if (cancel_restore_) return;
1523
1524 // Backend returns empty elements for collection items which it couldn't match (because they got deleted); we don't need those
1525 QMutableListIterator<PlaylistItemPtr> it(items);
1526 while (it.hasNext()) {
1527 PlaylistItemPtr item = it.next();
1528
1529 if (item->IsLocalCollectionItem() && item->Metadata().url().isEmpty()) {
1530 it.remove();
1531 }
1532 }
1533
1534 is_loading_ = true;
1535 InsertItems(items, 0);
1536 is_loading_ = false;
1537
1538 PlaylistBackend::Playlist p = backend_->GetPlaylist(id_);
1539
1540 // The newly loaded list of items might be shorter than it was before so look out for a bad last_played index
1541 last_played_item_index_ = p.last_played == -1 || p.last_played >= rowCount() ? QModelIndex() : index(p.last_played);
1542
1543 if (p.dynamic_type == PlaylistGenerator::Type_Query) {
1544 PlaylistGeneratorPtr gen = PlaylistGenerator::Create(p.dynamic_type);
1545 if (gen) {
1546
1547 CollectionBackend *backend = nullptr;
1548 if (p.dynamic_backend == collection_->songs_table()) backend = collection_;
1549
1550 if (backend) {
1551 gen->set_collection(backend);
1552 gen->Load(p.dynamic_data);
1553 TurnOnDynamicPlaylist(gen);
1554 }
1555
1556 }
1557 }
1558
1559 emit RestoreFinished();
1560
1561 QSettings s;
1562 s.beginGroup(kSettingsGroup);
1563 bool greyout = s.value("greyout_songs_startup", true).toBool();
1564 s.endGroup();
1565
1566 // Should we gray out deleted songs asynchronously on startup?
1567 if (greyout) {
1568 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
1569 (void)QtConcurrent::run(&Playlist::InvalidateDeletedSongs, this);
1570 #else
1571 (void)QtConcurrent::run(this, &Playlist::InvalidateDeletedSongs);
1572 #endif
1573 }
1574
1575 emit PlaylistLoaded();
1576
1577 }
1578
DescendingIntLessThan(int a,int b)1579 static bool DescendingIntLessThan(int a, int b) { return a > b; }
1580
RemoveItemsWithoutUndo(const QList<int> & indicesIn)1581 void Playlist::RemoveItemsWithoutUndo(const QList<int> &indicesIn) {
1582
1583 // Sort the indices descending because removing elements 'backwards' is easier - indices don't 'move' in the process.
1584 QList<int> indices = indicesIn;
1585 std::sort(indices.begin(), indices.end(), DescendingIntLessThan);
1586
1587 for (int j = 0; j < indices.count(); j++) {
1588 int beginning = indices[j], end = indices[j];
1589
1590 // Splits the indices into sequences. For example this: [1, 2, 4], will get split into [1, 2] and [4].
1591 while (j != indices.count() - 1 && indices[j] == indices[j + 1] + 1) {
1592 beginning--;
1593 j++;
1594 }
1595
1596 // Remove the current sequence.
1597 removeRows(beginning, end - beginning + 1);
1598 }
1599
1600 }
1601
removeRows(int row,int count,const QModelIndex & parent)1602 bool Playlist::removeRows(int row, int count, const QModelIndex &parent) {
1603
1604 Q_UNUSED(parent);
1605
1606 if (row < 0 || row >= items_.size() || row + count > items_.size()) {
1607 return false;
1608 }
1609
1610 if (count > kUndoItemLimit) {
1611 // Too big to keep in the undo stack. Also clear the stack because it might have been invalidated.
1612 RemoveItemsWithoutUndo(row, count);
1613 undo_stack_->clear();
1614 }
1615 else {
1616 undo_stack_->push(new PlaylistUndoCommands::RemoveItems(this, row, count));
1617 }
1618
1619 return true;
1620
1621 }
1622
removeRows(QList<int> & rows)1623 bool Playlist::removeRows(QList<int> &rows) {
1624
1625 if (rows.isEmpty()) {
1626 return false;
1627 }
1628
1629 // Start from the end to be sure that indices won't 'move' during the removal process
1630 std::sort(rows.begin(), rows.end(), std::greater<int>());
1631
1632 QList<int> part;
1633 while (!rows.isEmpty()) {
1634 // we're splitting the input list into sequences of consecutive numbers
1635 part.append(rows.takeFirst());
1636 while (!rows.isEmpty() && rows.first() == part.last() - 1) {
1637 part.append(rows.takeFirst());
1638 }
1639
1640 // and now we're removing the current sequence
1641 if (!removeRows(part.last(), part.size())) {
1642 return false;
1643 }
1644
1645 part.clear();
1646 }
1647
1648 return true;
1649
1650 }
1651
RemoveItemsWithoutUndo(const int row,const int count)1652 PlaylistItemList Playlist::RemoveItemsWithoutUndo(const int row, const int count) {
1653
1654 if (row < 0 || row >= items_.size() || row + count > items_.size()) {
1655 return PlaylistItemList();
1656 }
1657 beginRemoveRows(QModelIndex(), row, row + count - 1);
1658
1659 // Remove items
1660 PlaylistItemList ret;
1661 ret.reserve(count);
1662 for (int i = 0; i < count; ++i) {
1663 PlaylistItemPtr item(items_.takeAt(row));
1664 ret << item;
1665
1666 if (item->source() == Song::Source_Collection) {
1667 int id = item->Metadata().id();
1668 if (id != -1 && collection_items_by_id_.contains(id, item)) {
1669 collection_items_by_id_.remove(id, item);
1670 }
1671 }
1672 }
1673
1674 endRemoveRows();
1675
1676 QList<int>::iterator it = virtual_items_.begin();
1677 while (it != virtual_items_.end()) {
1678 if (*it >= items_.count()) {
1679 it = virtual_items_.erase(it); // clazy:exclude=strict-iterators
1680 }
1681 else {
1682 ++it;
1683 }
1684 }
1685
1686 // Reset current_virtual_index_
1687 if (current_row() == -1) {
1688 if (row - 1 > 0 && row - 1 < items_.size()) {
1689 current_virtual_index_ = virtual_items_.indexOf(row - 1);
1690 }
1691 else {
1692 current_virtual_index_ = -1;
1693 }
1694 }
1695 else {
1696 current_virtual_index_ = virtual_items_.indexOf(current_row());
1697 }
1698
1699 ScheduleSave();
1700
1701 return ret;
1702
1703 }
1704
StopAfter(const int row)1705 void Playlist::StopAfter(const int row) {
1706
1707 QModelIndex old_stop_after = stop_after_;
1708
1709 if ((stop_after_.isValid() && stop_after_.row() == row) || row == -1) {
1710 stop_after_ = QModelIndex();
1711 }
1712 else {
1713 stop_after_ = index(row, 0);
1714 }
1715
1716 if (old_stop_after.isValid()) {
1717 emit dataChanged(old_stop_after, old_stop_after.sibling(old_stop_after.row(), ColumnCount - 1));
1718 }
1719 if (stop_after_.isValid()) {
1720 emit dataChanged(stop_after_, stop_after_.sibling(stop_after_.row(), ColumnCount - 1));
1721 }
1722
1723 }
1724
SetStreamMetadata(const QUrl & url,const Song & song,const bool minor)1725 void Playlist::SetStreamMetadata(const QUrl &url, const Song &song, const bool minor) {
1726
1727 if (!current_item() || current_item()->Url() != url) return;
1728
1729 bool update_scrobble_point = song.length_nanosec() != current_item_metadata().length_nanosec();
1730 current_item()->SetTemporaryMetadata(song);
1731 if (update_scrobble_point) UpdateScrobblePoint();
1732 InformOfCurrentSongChange(AutoScroll_Never, minor);
1733
1734 }
1735
ClearStreamMetadata()1736 void Playlist::ClearStreamMetadata() {
1737
1738 if (!current_item()) return;
1739
1740 current_item()->ClearTemporaryMetadata();
1741 UpdateScrobblePoint();
1742
1743 emit dataChanged(index(current_item_index_.row(), 0), index(current_item_index_.row(), ColumnCount - 1));
1744
1745 }
1746
stop_after_current() const1747 bool Playlist::stop_after_current() const {
1748
1749 PlaylistSequence::RepeatMode repeat_mode = playlist_sequence_->repeat_mode();
1750 if (repeat_mode == PlaylistSequence::Repeat_OneByOne) {
1751 return true;
1752 }
1753
1754 return stop_after_.isValid() && current_item_index_.isValid() && stop_after_.row() == current_item_index_.row();
1755
1756 }
1757
current_item() const1758 PlaylistItemPtr Playlist::current_item() const {
1759
1760 // QList[] runs in constant time, so no need to cache current_item
1761 if (current_item_index_.isValid() && current_item_index_.row() <= items_.length()) {
1762 return items_[current_item_index_.row()];
1763 }
1764
1765 return PlaylistItemPtr();
1766
1767 }
1768
current_item_options() const1769 PlaylistItem::Options Playlist::current_item_options() const {
1770 if (!current_item()) return PlaylistItem::Default;
1771 return current_item()->options();
1772 }
1773
current_item_metadata() const1774 Song Playlist::current_item_metadata() const {
1775 if (!current_item()) return Song();
1776 return current_item()->Metadata();
1777 }
1778
Clear()1779 void Playlist::Clear() {
1780
1781 // If loading songs from session restore async, don't insert them
1782 cancel_restore_ = true;
1783
1784 const int count = items_.count();
1785
1786 if (count > kUndoItemLimit) {
1787 // Too big to keep in the undo stack. Also clear the stack because it might have been invalidated.
1788 RemoveItemsWithoutUndo(0, count);
1789 undo_stack_->clear();
1790 }
1791 else {
1792 undo_stack_->push(new PlaylistUndoCommands::RemoveItems(this, 0, count));
1793 }
1794
1795 TurnOffDynamicPlaylist();
1796
1797 ScheduleSave();
1798
1799 }
1800
RepopulateDynamicPlaylist()1801 void Playlist::RepopulateDynamicPlaylist() {
1802
1803 if (!dynamic_playlist_) return;
1804
1805 RemoveItemsNotInQueue();
1806 InsertSmartPlaylist(dynamic_playlist_);
1807
1808 }
1809
ExpandDynamicPlaylist()1810 void Playlist::ExpandDynamicPlaylist() {
1811
1812 if (!dynamic_playlist_) return;
1813
1814 InsertDynamicItems(5);
1815
1816 }
1817
RemoveItemsNotInQueue()1818 void Playlist::RemoveItemsNotInQueue() {
1819
1820 if (queue_->is_empty() && !current_item_index_.isValid()) {
1821 RemoveItemsWithoutUndo(0, items_.count());
1822 return;
1823 }
1824
1825 int start = 0;
1826 forever {
1827 // Find a place to start - first row that isn't in the queue
1828 forever {
1829 if (start >= rowCount()) return;
1830 if (!queue_->ContainsSourceRow(start) && current_row() != start) break;
1831 start++;
1832 }
1833
1834 // Figure out how many rows to remove - keep going until we find a row that is in the queue
1835 int count = 1;
1836 forever {
1837 if (start + count >= rowCount()) break;
1838 if (queue_->ContainsSourceRow(start + count) || current_row() == start + count) break;
1839 count++;
1840 }
1841
1842 RemoveItemsWithoutUndo(start, count);
1843 start++;
1844 }
1845
1846 }
1847
ReloadItems(const QList<int> & rows)1848 void Playlist::ReloadItems(const QList<int> &rows) {
1849
1850 for (int row : rows) {
1851 PlaylistItemPtr item = item_at(row);
1852 QPersistentModelIndex idx = index(row, 0);
1853 if (idx.isValid()) {
1854 ItemReload(idx, item->Metadata(), false);
1855 }
1856 }
1857
1858 }
1859
ReloadItemsBlocking(const QList<int> & rows)1860 void Playlist::ReloadItemsBlocking(const QList<int> &rows) {
1861
1862 for (int row : rows) {
1863 PlaylistItemPtr item = item_at(row);
1864 Song old_metadata = item->Metadata();
1865 item->Reload();
1866 QPersistentModelIndex idx = index(row, 0);
1867 ItemReloadComplete(idx, old_metadata, false);
1868 }
1869
1870 }
1871
AddSongInsertVetoListener(SongInsertVetoListener * listener)1872 void Playlist::AddSongInsertVetoListener(SongInsertVetoListener *listener) {
1873 veto_listeners_.append(listener);
1874 QObject::connect(listener, &SongInsertVetoListener::destroyed, this, &Playlist::SongInsertVetoListenerDestroyed);
1875 }
1876
RemoveSongInsertVetoListener(SongInsertVetoListener * listener)1877 void Playlist::RemoveSongInsertVetoListener(SongInsertVetoListener *listener) {
1878 QObject::disconnect(listener, &SongInsertVetoListener::destroyed, this, &Playlist::SongInsertVetoListenerDestroyed);
1879 veto_listeners_.removeAll(listener);
1880 }
1881
SongInsertVetoListenerDestroyed()1882 void Playlist::SongInsertVetoListenerDestroyed() {
1883 veto_listeners_.removeAll(qobject_cast<SongInsertVetoListener*>(sender()));
1884 }
1885
Shuffle()1886 void Playlist::Shuffle() {
1887
1888 PlaylistItemList new_items(items_);
1889
1890 int begin = 0;
1891 if (current_item_index_.isValid()) {
1892 if (new_items[0] != new_items[current_item_index_.row()]) {
1893 std::swap(new_items[0], new_items[current_item_index_.row()]);
1894 }
1895 begin = 1;
1896 }
1897
1898 if (dynamic_playlist_ && current_item_index_.isValid()) {
1899 begin += current_item_index_.row() + 1;
1900 }
1901
1902 const int count = items_.count();
1903 for (int i = begin; i < count; ++i) {
1904 int new_pos = i + (rand() % (count - i));
1905
1906 std::swap(new_items[i], new_items[new_pos]);
1907 }
1908
1909 undo_stack_->push(new PlaylistUndoCommands::ShuffleItems(this, new_items));
1910
1911 }
1912
1913 namespace {
1914
AlbumShuffleComparator(const QMap<QString,int> & album_key_positions,const QMap<int,QString> & album_keys,const int left,const int right)1915 bool AlbumShuffleComparator(const QMap<QString, int> &album_key_positions, const QMap<int, QString> &album_keys, const int left, const int right) {
1916
1917 const int left_pos = album_key_positions[album_keys[left]];
1918 const int right_pos = album_key_positions[album_keys[right]];
1919
1920 if (left_pos == right_pos) return left < right;
1921 return left_pos < right_pos;
1922
1923 }
1924
1925 } // namespace
1926
ReshuffleIndices()1927 void Playlist::ReshuffleIndices() {
1928
1929 if (!playlist_sequence_) {
1930 return;
1931 }
1932
1933 if (playlist_sequence_->shuffle_mode() == PlaylistSequence::Shuffle_Off) {
1934 // No shuffling - sort the virtual item list normally.
1935 std::sort(virtual_items_.begin(), virtual_items_.end());
1936 if (current_row() != -1) {
1937 current_virtual_index_ = virtual_items_.indexOf(current_row());
1938 }
1939 return;
1940 }
1941
1942 // If the user is already playing a song, advance the begin iterator to only shuffle items that haven't been played yet.
1943 QList<int>::iterator begin = virtual_items_.begin();
1944 QList<int>::iterator end = virtual_items_.end();
1945 if (current_virtual_index_ != -1) {
1946 std::advance(begin, current_virtual_index_ + 1);
1947 }
1948
1949 std::random_device rd;
1950 std::mt19937 g(rd());
1951
1952 switch (playlist_sequence_->shuffle_mode()) {
1953 case PlaylistSequence::Shuffle_Off:
1954 // Handled above.
1955 break;
1956
1957 case PlaylistSequence::Shuffle_All:
1958 case PlaylistSequence::Shuffle_InsideAlbum:
1959 std::shuffle(begin, end, g);
1960 break;
1961
1962 case PlaylistSequence::Shuffle_Albums: {
1963 QMap<int, QString> album_keys; // real index -> key
1964 QSet<QString> album_key_set; // unique keys
1965
1966 // Find all the unique albums in the playlist
1967 for (QList<int>::iterator it = begin; it != end; ++it) {
1968 const int index = *it;
1969 const QString key = items_[index]->Metadata().AlbumKey();
1970 album_keys[index] = key;
1971 album_key_set << key;
1972 }
1973
1974 // Shuffle them
1975 QStringList shuffled_album_keys = album_key_set.values();
1976 std::shuffle(shuffled_album_keys.begin(), shuffled_album_keys.end(), g);
1977
1978 // If the user is currently playing a song, force its album to be first
1979 // Or if the song was not playing but it was selected, force its album to be first.
1980 if (current_virtual_index_ != -1 || current_row() != -1) {
1981 const QString key = items_[current_row()]->Metadata().AlbumKey();
1982 const int pos = shuffled_album_keys.indexOf(key);
1983 if (pos >= 1) {
1984 std::swap(shuffled_album_keys[0], shuffled_album_keys[pos]);
1985 }
1986 }
1987
1988 // Create album key -> position mapping for fast lookup
1989 QMap<QString, int> album_key_positions;
1990 for (int i = 0; i < shuffled_album_keys.count(); ++i) {
1991 album_key_positions[shuffled_album_keys[i]] = i;
1992 }
1993
1994 // Sort the virtual items
1995 std::stable_sort(begin, end, std::bind(AlbumShuffleComparator, album_key_positions, album_keys, std::placeholders::_1, std::placeholders::_2));
1996
1997 break;
1998 }
1999 }
2000
2001 }
2002
set_sequence(PlaylistSequence * v)2003 void Playlist::set_sequence(PlaylistSequence *v) {
2004
2005 playlist_sequence_ = v;
2006 QObject::connect(v, &PlaylistSequence::ShuffleModeChanged, this, &Playlist::ShuffleModeChanged);
2007
2008 ShuffleModeChanged(v->shuffle_mode());
2009
2010 }
2011
proxy() const2012 QSortFilterProxyModel *Playlist::proxy() const { return proxy_; }
2013
GetAllSongs() const2014 SongList Playlist::GetAllSongs() const {
2015
2016 SongList ret;
2017 ret.reserve(items_.count());
2018 for (PlaylistItemPtr item : items_) { // clazy:exclude=range-loop-reference
2019 ret << item->Metadata();
2020 }
2021 return ret;
2022
2023 }
2024
GetAllItems() const2025 PlaylistItemList Playlist::GetAllItems() const { return items_; }
2026
GetTotalLength() const2027 quint64 Playlist::GetTotalLength() const {
2028
2029 quint64 ret = 0;
2030 for (PlaylistItemPtr item : items_) { // clazy:exclude=range-loop-reference
2031 qint64 length = item->Metadata().length_nanosec();
2032 if (length > 0) ret += length;
2033 }
2034 return ret;
2035
2036 }
2037
collection_items_by_id(const int id) const2038 PlaylistItemList Playlist::collection_items_by_id(const int id) const {
2039 return collection_items_by_id_.values(id);
2040 }
2041
TracksAboutToBeDequeued(const QModelIndex &,int begin,int end)2042 void Playlist::TracksAboutToBeDequeued(const QModelIndex&, int begin, int end) {
2043
2044 for (int i = begin; i <= end; ++i) {
2045 temp_dequeue_change_indexes_ << queue_->mapToSource(queue_->index(i, Column_Title));
2046 }
2047
2048 }
2049
TracksDequeued()2050 void Playlist::TracksDequeued() {
2051
2052 for (const QModelIndex &idx : temp_dequeue_change_indexes_) {
2053 emit dataChanged(idx, idx);
2054 }
2055 temp_dequeue_change_indexes_.clear();
2056 emit QueueChanged();
2057
2058 }
2059
TracksEnqueued(const QModelIndex &,const int begin,const int end)2060 void Playlist::TracksEnqueued(const QModelIndex&, const int begin, const int end) {
2061
2062 const QModelIndex &b = queue_->mapToSource(queue_->index(begin, Column_Title));
2063 const QModelIndex &e = queue_->mapToSource(queue_->index(end, Column_Title));
2064 emit dataChanged(b, e);
2065
2066 }
2067
QueueLayoutChanged()2068 void Playlist::QueueLayoutChanged() {
2069
2070 for (int i = 0; i < queue_->rowCount(); ++i) {
2071 const QModelIndex &idx = queue_->mapToSource(queue_->index(i, Column_Title));
2072 emit dataChanged(idx, idx);
2073 }
2074
2075 }
2076
ItemChanged(const int row)2077 void Playlist::ItemChanged(const int row) {
2078
2079 QModelIndex idx = index(row, ColumnCount - 1);
2080 if (idx.isValid()) {
2081 emit dataChanged(index(row, 0), index(row, ColumnCount - 1));
2082 }
2083
2084 }
2085
ItemChanged(PlaylistItemPtr item)2086 void Playlist::ItemChanged(PlaylistItemPtr item) {
2087
2088 for (int row = 0; row < items_.count(); ++row) {
2089 if (items_[row] == item) {
2090 ItemChanged(row);
2091 }
2092 }
2093
2094 }
2095
InformOfCurrentSongChange(const AutoScroll autoscroll,const bool minor)2096 void Playlist::InformOfCurrentSongChange(const AutoScroll autoscroll, const bool minor) {
2097
2098 // If the song is invalid, we won't play it - there's no point in informing anybody about the change
2099 const Song metadata(current_item_metadata());
2100 if (metadata.is_valid()) {
2101 if (minor) {
2102 emit SongMetadataChanged(metadata);
2103 if (editing_ != current_item_index_.row()) {
2104 emit dataChanged(index(current_item_index_.row(), 0), index(current_item_index_.row(), ColumnCount - 1));
2105 }
2106 }
2107 else {
2108 emit CurrentSongChanged(metadata);
2109 emit MaybeAutoscroll(autoscroll);
2110 emit dataChanged(index(current_item_index_.row(), 0), index(current_item_index_.row(), ColumnCount - 1));
2111 }
2112 }
2113
2114 }
2115
InvalidateDeletedSongs()2116 void Playlist::InvalidateDeletedSongs() {
2117
2118 QList<int> invalidated_rows;
2119
2120 for (int row = 0; row < items_.count(); ++row) {
2121 PlaylistItemPtr item = items_[row];
2122 Song song = item->Metadata();
2123
2124 if (song.url().isLocalFile()) {
2125 bool exists = QFile::exists(song.url().toLocalFile());
2126
2127 if (!exists && !item->HasForegroundColor(kInvalidSongPriority)) {
2128 // gray out the song if it's not there
2129 item->SetForegroundColor(kInvalidSongPriority, kInvalidSongColor);
2130 invalidated_rows.append(row); // clazy:exclude=reserve-candidates
2131 }
2132 else if (exists && item->HasForegroundColor(kInvalidSongPriority)) {
2133 item->RemoveForegroundColor(kInvalidSongPriority);
2134 invalidated_rows.append(row); // clazy:exclude=reserve-candidates
2135 }
2136 }
2137 }
2138
2139 if (!invalidated_rows.isEmpty()) {
2140 if (QThread::currentThread() == thread()) {
2141 ReloadItems(invalidated_rows);
2142 }
2143 else {
2144 ReloadItemsBlocking(invalidated_rows);
2145 }
2146 }
2147
2148 }
2149
RemoveDeletedSongs()2150 void Playlist::RemoveDeletedSongs() {
2151
2152 QList<int> rows_to_remove;
2153
2154 for (int row = 0; row < items_.count(); ++row) {
2155 PlaylistItemPtr item = items_[row];
2156 Song song = item->Metadata();
2157
2158 if (song.url().isLocalFile() && !QFile::exists(song.url().toLocalFile())) {
2159 rows_to_remove.append(row); // clazy:exclude=reserve-candidates
2160 }
2161 }
2162
2163 removeRows(rows_to_remove);
2164
2165 }
2166
2167 namespace {
2168
2169 struct SongSimilarHash {
operator ()__anon0a34c4480411::SongSimilarHash2170 size_t operator() (const Song &song) const {
2171 return HashSimilar(song);
2172 }
2173 };
2174
2175 struct SongSimilarEqual {
operator ()__anon0a34c4480411::SongSimilarEqual2176 size_t operator()(const Song &song1, const Song &song2) const {
2177 return song1.IsSimilar(song2);
2178 }
2179 };
2180
2181 } // namespace
2182
RemoveDuplicateSongs()2183 void Playlist::RemoveDuplicateSongs() {
2184
2185 QList<int> rows_to_remove;
2186 std::unordered_map<Song, int, SongSimilarHash, SongSimilarEqual> unique_songs;
2187
2188 for (int row = 0; row < items_.count(); ++row) {
2189 PlaylistItemPtr item = items_[row];
2190 const Song &song = item->Metadata();
2191
2192 bool found_duplicate = false;
2193
2194 auto uniq_song_it = unique_songs.find(song);
2195 if (uniq_song_it != unique_songs.end()) {
2196 const Song &uniq_song = uniq_song_it->first;
2197
2198 if (song.bitrate() > uniq_song.bitrate()) {
2199 rows_to_remove.append(unique_songs[uniq_song]); // clazy:exclude=reserve-candidates
2200 unique_songs.erase(uniq_song);
2201 unique_songs.insert(std::make_pair(song, row));
2202 }
2203 else {
2204 rows_to_remove.append(row); // clazy:exclude=reserve-candidates
2205 }
2206 found_duplicate = true;
2207 }
2208
2209 if (!found_duplicate) {
2210 unique_songs.insert(std::make_pair(song, row));
2211 }
2212 }
2213
2214 removeRows(rows_to_remove);
2215
2216 }
2217
RemoveUnavailableSongs()2218 void Playlist::RemoveUnavailableSongs() {
2219
2220 QList<int> rows_to_remove;
2221 for (int row = 0; row < items_.count(); ++row) {
2222 PlaylistItemPtr item = items_[row];
2223 const Song &song = item->Metadata();
2224
2225 // Check only local files
2226 if (song.url().isLocalFile() && !QFile::exists(song.url().toLocalFile())) {
2227 rows_to_remove.append(row); // clazy:exclude=reserve-candidates
2228 }
2229 }
2230
2231 removeRows(rows_to_remove);
2232
2233 }
2234
ApplyValidityOnCurrentSong(const QUrl & url,const bool valid)2235 bool Playlist::ApplyValidityOnCurrentSong(const QUrl &url, const bool valid) {
2236
2237 PlaylistItemPtr current = current_item();
2238
2239 if (current) {
2240 Song current_song = current->Metadata();
2241
2242 // If validity has changed, reload the item
2243 if (current_song.source() == Song::Source_LocalFile || current_song.source() == Song::Source_Collection) {
2244 if (current_song.url() == url && current_song.url().isLocalFile() && current_song.is_valid() != QFile::exists(current_song.url().toLocalFile())) {
2245 ReloadItems(QList<int>() << current_row());
2246 }
2247 }
2248
2249 // Gray out the song if it's now broken; otherwise undo the gray color
2250 if (valid) {
2251 current->RemoveForegroundColor(kInvalidSongPriority);
2252 }
2253 else {
2254 current->SetForegroundColor(kInvalidSongPriority, kInvalidSongColor);
2255 }
2256 }
2257
2258 return static_cast<bool>(current);
2259
2260 }
2261
SetColumnAlignment(const ColumnAlignmentMap & alignment)2262 void Playlist::SetColumnAlignment(const ColumnAlignmentMap &alignment) {
2263 column_alignments_ = alignment;
2264 }
2265
SkipTracks(const QModelIndexList & source_indexes)2266 void Playlist::SkipTracks(const QModelIndexList &source_indexes) {
2267
2268 for (const QModelIndex &source_index : source_indexes) {
2269 PlaylistItemPtr track_to_skip = item_at(source_index.row());
2270 track_to_skip->SetShouldSkip(!((track_to_skip)->GetShouldSkip()));
2271 emit dataChanged(source_index, source_index);
2272 }
2273
2274 }
2275
UpdateScrobblePoint(const qint64 seek_point_nanosec)2276 void Playlist::UpdateScrobblePoint(const qint64 seek_point_nanosec) {
2277
2278 const qint64 length = current_item_metadata().length_nanosec();
2279
2280 if (seek_point_nanosec <= 0) {
2281 if (length == 0) {
2282 scrobble_point_ = kMaxScrobblePointNsecs;
2283 }
2284 else {
2285 scrobble_point_ = qBound(kMinScrobblePointNsecs, length / 2, kMaxScrobblePointNsecs);
2286 }
2287 }
2288 else {
2289 if (length <= 0) {
2290 scrobble_point_ = seek_point_nanosec + kMaxScrobblePointNsecs;
2291 }
2292 else {
2293 scrobble_point_ = qBound(seek_point_nanosec + kMinScrobblePointNsecs, seek_point_nanosec + (length / 2), seek_point_nanosec + kMaxScrobblePointNsecs);
2294 }
2295 }
2296
2297 scrobbled_ = false;
2298
2299 }
2300
AlbumCoverLoaded(const Song & song,const AlbumCoverLoaderResult & result)2301 void Playlist::AlbumCoverLoaded(const Song &song, const AlbumCoverLoaderResult &result) {
2302
2303 // Update art_manual for local songs that are not in the collection.
2304 if (((result.type == AlbumCoverLoaderResult::Type_Manual && result.album_cover.cover_url.isLocalFile()) || result.type == AlbumCoverLoaderResult::Type_ManuallyUnset) && (song.source() == Song::Source_LocalFile || song.source() == Song::Source_CDDA || song.source() == Song::Source_Device)) {
2305 PlaylistItemPtr item = current_item();
2306 if (item && item->Metadata() == song && (!item->Metadata().art_manual_is_valid() || (result.type == AlbumCoverLoaderResult::Type_ManuallyUnset && !item->Metadata().has_manually_unset_cover()))) {
2307 qLog(Debug) << "Updating art manual for local song" << song.title() << song.album() << song.title() << "to" << result.album_cover.cover_url << "in playlist.";
2308 item->SetArtManual(result.album_cover.cover_url);
2309 ScheduleSaveAsync();
2310 }
2311 }
2312
2313 }
2314
dynamic_history_length() const2315 int Playlist::dynamic_history_length() const {
2316 return dynamic_playlist_ && last_played_item_index_.isValid() ? last_played_item_index_.row() + 1 : 0;
2317 }
2318
TurnOffDynamicPlaylist()2319 void Playlist::TurnOffDynamicPlaylist() {
2320
2321 dynamic_playlist_.reset();
2322
2323 if (playlist_sequence_) {
2324 ShuffleModeChanged(playlist_sequence_->shuffle_mode());
2325 }
2326
2327 emit DynamicModeChanged(false);
2328
2329 ScheduleSave();
2330
2331 }
2332
RateSong(const QModelIndex & idx,const double rating)2333 void Playlist::RateSong(const QModelIndex &idx, const double rating) {
2334
2335 if (has_item_at(idx.row())) {
2336 PlaylistItemPtr item = item_at(idx.row());
2337 if (item && item->IsLocalCollectionItem() && item->Metadata().id() != -1) {
2338 collection_->UpdateSongRatingAsync(item->Metadata().id(), rating);
2339 }
2340 }
2341
2342 }
2343
RateSongs(const QModelIndexList & index_list,const double rating)2344 void Playlist::RateSongs(const QModelIndexList &index_list, const double rating) {
2345
2346 QList<int> id_list;
2347 for (const QModelIndex &idx : index_list) {
2348 const int row = idx.row();
2349 if (has_item_at(row)) {
2350 PlaylistItemPtr item = item_at(row);
2351 if (item && item->IsLocalCollectionItem() && item->Metadata().id() != -1) {
2352 id_list << item->Metadata().id(); // clazy:exclude=reserve-candidates
2353 }
2354 }
2355 }
2356 collection_->UpdateSongsRatingAsync(id_list, rating);
2357
2358 }
2359