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 &current = 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