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