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