40
41
#include "smartplaylists/generatorinserter.h"
41
42
#include "smartplaylists/generatormimedata.h"
44
44
#include <QApplication>
46
#include <QDirIterator>
47
47
#include <QFileInfo>
48
#include <QDirIterator>
49
#include <QMutableListIterator>
50
#include <QSortFilterProxyModel>
49
51
#include <QUndoStack>
50
#include <QSortFilterProxyModel>
52
#include <QtConcurrentRun>
52
55
#include <boost/bind.hpp>
53
56
#include <algorithm>
55
#include <lastfm/ScrobblePoint>
57
58
using smart_playlists::Generator;
58
59
using smart_playlists::GeneratorInserter;
59
60
using smart_playlists::GeneratorPtr;
63
64
const char* Playlist::kRowsMimetype = "application/x-clementine-playlist-rows";
64
65
const char* Playlist::kPlayNowMimetype = "application/x-clementine-play-now";
67
const int Playlist::kInvalidSongPriority = 200;
68
const QRgb Playlist::kInvalidSongColor = qRgb(0xC0, 0xC0, 0xC0);
70
const int Playlist::kDynamicHistoryPriority = 100;
71
const QRgb Playlist::kDynamicHistoryColor = qRgb(0x80, 0x80, 0x80);
73
const char* Playlist::kSettingsGroup = "Playlist";
66
75
Playlist::Playlist(PlaylistBackend* backend,
67
76
TaskManager* task_manager,
68
77
LibraryBackend* library,
101
110
SLOT(TracksEnqueued(const QModelIndex&, int, int)));
103
112
connect(queue_, SIGNAL(layoutChanged()), SLOT(QueueLayoutChanged()));
114
column_alignments_[Column_Length]
115
= column_alignments_[Column_Track]
116
= column_alignments_[Column_Disc]
117
= column_alignments_[Column_Year]
118
= column_alignments_[Column_BPM]
119
= column_alignments_[Column_Bitrate]
120
= column_alignments_[Column_Samplerate]
121
= column_alignments_[Column_Filesize]
122
= column_alignments_[Column_PlayCount]
123
= column_alignments_[Column_SkipCount]
124
= (Qt::AlignRight | Qt::AlignVCenter);
126
column_alignments_[Column_Score] = (Qt::AlignCenter);
106
129
Playlist::~Playlist() {
111
134
template<typename T>
112
QModelIndex InsertSongItems(Playlist* playlist, const SongList& songs, int pos) {
135
void Playlist::InsertSongItems(const SongList& songs, int pos, bool play_now, bool enqueue) {
113
136
PlaylistItemList items;
114
138
foreach (const Song& song, songs) {
115
139
items << PlaylistItemPtr(new T(song));
117
return playlist->InsertItems(items, pos);
142
InsertItems(items, pos, play_now, enqueue);
120
145
QVariant Playlist::headerData(int section, Qt::Orientation, int role) const {
221
247
// Don't forget to change Playlist::CompareItems when adding new columns
222
248
switch (index.column()) {
223
249
case Column_Title:
224
if (!song.title().isEmpty())
226
if (!song.basefilename().isEmpty())
227
return song.basefilename();
228
return song.filename();
250
return song.PrettyTitle();
229
251
case Column_Artist: return song.artist();
230
252
case Column_Album: return song.album();
231
case Column_Length: return song.length();
253
case Column_Length: return song.length_nanosec();
232
254
case Column_Track: return song.track();
233
255
case Column_Disc: return song.disc();
234
256
case Column_Year: return song.year();
262
284
case Qt::TextAlignmentRole:
263
switch (index.column()) {
270
case Column_Samplerate:
271
case Column_Filesize:
272
case Column_PlayCount:
273
case Column_SkipCount:
274
return QVariant(Qt::AlignRight | Qt::AlignVCenter);
276
return QVariant(Qt::AlignCenter | Qt::AlignVCenter);
278
return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
285
return QVariant(column_alignments_.value(index.column(), (Qt::AlignLeft | Qt::AlignVCenter)));
281
287
case Qt::ForegroundRole:
282
if (items_[index.row()]->IsDynamicHistory())
283
return QApplication::palette().brush(QPalette::Disabled, QPalette::Text);
288
if (items_[index.row()]->HasCurrentForegroundColor()) {
289
return QBrush(items_[index.row()]->GetCurrentForegroundColor());
293
case Qt::BackgroundRole:
294
if (items_[index.row()]->HasCurrentBackgroundColor()) {
295
return QBrush(items_[index.row()]->GetCurrentBackgroundColor());
284
297
return QVariant();
336
int Playlist::current_index() const {
349
int Playlist::current_row() const {
337
350
return current_item_index_.isValid() ? current_item_index_.row() : -1;
340
int Playlist::last_played_index() const {
353
const QModelIndex Playlist::current_index() const {
354
return current_item_index_;
357
int Playlist::last_played_row() const {
341
358
return last_played_item_index_.isValid() ? last_played_item_index_.row() : -1;
463
480
// Still off the end? Then just give up
464
if (next_virtual_index >= virtual_items_.count())
481
if (next_virtual_index < 0 || next_virtual_index >= virtual_items_.count())
467
484
return virtual_items_[next_virtual_index];
470
int Playlist::previous_index() const {
487
int Playlist::previous_row() const {
471
488
int prev_virtual_index = PreviousVirtualIndex(current_virtual_index_);
472
489
if (prev_virtual_index < 0) {
473
490
// We've gone off the beginning of the playlist.
516
533
if (old_current.isValid()) {
517
534
if (dynamic_playlist_) {
518
items_[old_current.row()]->SetDynamicHistory(true);
535
items_[old_current.row()]->SetForegroundColor(kDynamicHistoryPriority,
536
kDynamicHistoryColor);
521
539
emit dataChanged(old_current, old_current.sibling(old_current.row(), ColumnCount-1));
524
542
if (current_item_index_.isValid()) {
525
emit dataChanged(current_item_index_, current_item_index_.sibling(current_item_index_.row(), ColumnCount-1));
526
emit CurrentSongChanged(current_item_metadata());
543
InformOfCurrentSongChange(current_item_index_,
544
current_item_index_.sibling(current_item_index_.row(), ColumnCount-1),
545
current_item_metadata());
529
548
// Update the virtual index
553
572
connect(inserter, SIGNAL(Error(QString)), SIGNAL(LoadTracksError(QString)));
554
573
connect(inserter, SIGNAL(PlayRequested(QModelIndex)), SIGNAL(PlayRequested(QModelIndex)));
556
inserter->Load(this, -1, false, dynamic_playlist_, count);
575
inserter->Load(this, -1, false, false, dynamic_playlist_, count);
559
578
// Remove the first item
593
612
using smart_playlists::GeneratorMimeData;
614
bool play_now = false;
615
bool enqueue_now = false;
616
if (const MimeData* mime_data = qobject_cast<const MimeData*>(data)) {
617
if (mime_data->clear_first_) {
620
play_now = mime_data->play_now_;
621
enqueue_now = mime_data->enqueue_now_;
595
624
if (const SongMimeData* song_data = qobject_cast<const SongMimeData*>(data)) {
596
625
// Dragged from a library
597
626
// We want to check if these songs are from the actual local file backend,
598
627
// if they are we treat them differently.
599
628
if (song_data->backend && song_data->backend->songs_table() == Library::kSongsTable)
600
InsertSongItems<LibraryPlaylistItem>(this, song_data->songs, row);
629
InsertSongItems<LibraryPlaylistItem>(song_data->songs, row, play_now, enqueue_now);
601
630
else if (song_data->backend && song_data->backend->songs_table() == MagnatuneService::kSongsTable)
602
InsertSongItems<MagnatunePlaylistItem>(this, song_data->songs, row);
631
InsertSongItems<MagnatunePlaylistItem>(song_data->songs, row, play_now, enqueue_now);
603
632
else if (song_data->backend && song_data->backend->songs_table() == JamendoService::kSongsTable)
604
InsertSongItems<JamendoPlaylistItem>(this, song_data->songs, row);
633
InsertSongItems<JamendoPlaylistItem>(song_data->songs, row, play_now, enqueue_now);
606
InsertSongItems<SongPlaylistItem>(this, song_data->songs, row);
635
InsertSongItems<SongPlaylistItem>(song_data->songs, row, play_now, enqueue_now);
607
636
} else if (const RadioMimeData* radio_data = qobject_cast<const RadioMimeData*>(data)) {
608
637
// Dragged from the Radio pane
609
InsertRadioStations(radio_data->items, row, data->hasFormat(kPlayNowMimetype));
638
InsertRadioStations(radio_data->model, radio_data->indexes,
639
row, play_now, enqueue_now);
610
640
} else if (const GeneratorMimeData* generator_data = qobject_cast<const GeneratorMimeData*>(data)) {
611
InsertSmartPlaylist(generator_data->generator_, row, data->hasFormat(kPlayNowMimetype));
641
InsertSmartPlaylist(generator_data->generator_, row, play_now, enqueue_now);
642
} else if (const PlaylistItemMimeData* item_data = qobject_cast<const PlaylistItemMimeData*>(data)) {
643
InsertItems(item_data->items_, row, play_now, enqueue_now);
612
644
} else if (data->hasFormat(kRowsMimetype)) {
613
645
// Dragged from the playlist
614
646
// Rearranging it is tricky...
643
675
} else if (data->hasUrls()) {
644
676
// URL list dragged from the file list or some other app
645
InsertUrls(data->urls(), false, row);
677
InsertUrls(data->urls(), row, play_now, enqueue_now);
651
void Playlist::InsertUrls(const QList<QUrl> &urls, bool play_now, int pos) {
652
SongLoaderInserter* inserter = new SongLoaderInserter(task_manager_, library_, this);
683
void Playlist::InsertUrls(const QList<QUrl> &urls, int pos, bool play_now, bool enqueue) {
684
SongLoaderInserter* inserter = new SongLoaderInserter(task_manager_, library_);
653
685
connect(inserter, SIGNAL(Error(QString)), SIGNAL(LoadTracksError(QString)));
654
connect(inserter, SIGNAL(PlayRequested(QModelIndex)), SIGNAL(PlayRequested(QModelIndex)));
656
inserter->Load(this, pos, play_now, urls);
687
inserter->Load(this, pos, play_now, enqueue, urls);
659
void Playlist::InsertSmartPlaylist(GeneratorPtr generator, int pos, bool play_now) {
690
void Playlist::InsertSmartPlaylist(GeneratorPtr generator, int pos, bool play_now, bool enqueue) {
691
// Hack: If the generator hasn't got a library set then use the main one
692
if (!generator->library()) {
693
generator->set_library(library_);
660
696
GeneratorInserter* inserter = new GeneratorInserter(task_manager_, library_, this);
661
697
connect(inserter, SIGNAL(Error(QString)), SIGNAL(LoadTracksError(QString)));
662
connect(inserter, SIGNAL(PlayRequested(QModelIndex)), SIGNAL(PlayRequested(QModelIndex)));
664
inserter->Load(this, pos, play_now, generator);
699
inserter->Load(this, pos, play_now, enqueue, generator);
666
701
if (generator->is_dynamic()) {
667
702
TurnOnDynamicPlaylist(generator);
759
794
changePersistentIndex(pidx, index(pidx.row() + d, pidx.column(), QModelIndex()));
762
current_virtual_index_ = virtual_items_.indexOf(current_index());
797
current_virtual_index_ = virtual_items_.indexOf(current_row());
768
QModelIndex Playlist::InsertItems(const PlaylistItemList& items, int pos) {
770
return QModelIndex();
803
void Playlist::InsertItems(const PlaylistItemList& itemsIn, int pos, bool play_now, bool enqueue) {
804
if (itemsIn.isEmpty())
807
PlaylistItemList items = itemsIn;
812
foreach(PlaylistItemPtr item, items) {
813
songs << item.get()->Metadata();
817
foreach(SongInsertVetoListener* listener, veto_listeners_) {
818
foreach(const Song& song, listener->AboutToInsertSongs(GetAllSongs(), songs)) {
823
if(!vetoed.isEmpty()) {
824
QMutableListIterator<PlaylistItemPtr> it(items);
825
while (it.hasNext()) {
826
PlaylistItemPtr item = it.next();
827
const Song& current = item.get()->Metadata();
829
if(vetoed.contains(current)) {
830
vetoed.removeOne(current);
835
// check for empty items once again after veto
836
if(items.isEmpty()) {
772
841
const int start = pos == -1 ? items_.count() : pos;
773
undo_stack_->push(new PlaylistUndoCommands::InsertItems(this, items, pos));
842
undo_stack_->push(new PlaylistUndoCommands::InsertItems(this, items, pos, enqueue));
775
return index(start, 0);
845
emit PlayRequested(index(start, 0));
778
QModelIndex Playlist::InsertItemsWithoutUndo(const PlaylistItemList& items,
848
void Playlist::InsertItemsWithoutUndo(const PlaylistItemList& items,
849
int pos, bool enqueue) {
780
850
if (items.isEmpty())
781
return QModelIndex();
783
853
const int start = pos == -1 ? items_.count() : pos;
784
854
const int end = start + items.count() - 1;
878
QModelIndexList indexes;
879
for (int i=start ; i<=end ; ++i) {
880
indexes << index(i, 0);
882
queue_->ToggleTracks(indexes);
808
886
ReshuffleIndices();
810
return index(start, 0);
813
QModelIndex Playlist::InsertLibraryItems(const SongList& songs, int pos) {
814
return InsertSongItems<LibraryPlaylistItem>(this, songs, pos);
817
QModelIndex Playlist::InsertSongs(const SongList& songs, int pos) {
818
return InsertSongItems<SongPlaylistItem>(this, songs, pos);
821
QModelIndex Playlist::InsertSongsOrLibraryItems(const SongList& songs, int pos) {
889
void Playlist::InsertLibraryItems(const SongList& songs, int pos, bool play_now, bool enqueue) {
890
InsertSongItems<LibraryPlaylistItem>(songs, pos, play_now, enqueue);
893
void Playlist::InsertSongs(const SongList& songs, int pos, bool play_now, bool enqueue) {
894
InsertSongItems<SongPlaylistItem>(songs, pos, play_now, enqueue);
897
void Playlist::InsertSongsOrLibraryItems(const SongList& songs, int pos, bool play_now, bool enqueue) {
822
898
PlaylistItemList items;
823
899
foreach (const Song& song, songs) {
824
900
if (song.id() == -1)
827
903
items << PlaylistItemPtr(new LibraryPlaylistItem(song));
829
return InsertItems(items, pos);
905
InsertItems(items, pos, play_now, enqueue);
832
QModelIndex Playlist::InsertRadioStations(const QList<RadioItem*>& items, int pos, bool play_now) {
908
void Playlist::InsertRadioStations(const RadioModel* model,
909
const QModelIndexList& items,
910
int pos, bool play_now, bool enqueue) {
833
911
PlaylistItemList playlist_items;
834
912
QList<QUrl> song_urls;
835
foreach (RadioItem* item, items) {
839
if (item->use_song_loader) {
840
song_urls << item->Url();
842
playlist_items << shared_ptr<PlaylistItem>(
843
new RadioPlaylistItem(item->service, item->Url(), item->Title(), item->Artist()));
914
foreach (const QModelIndex& item, items) {
915
switch (item.data(RadioModel::Role_PlayBehaviour).toInt()) {
916
case RadioModel::PlayBehaviour_SingleItem:
917
playlist_items << shared_ptr<PlaylistItem>(new RadioPlaylistItem(
918
model->ServiceForIndex(item),
919
item.data(RadioModel::Role_Url).toUrl(),
920
item.data(RadioModel::Role_Title).toString(),
921
item.data(RadioModel::Role_Artist).toString()));
924
case RadioModel::PlayBehaviour_UseSongLoader:
925
song_urls << item.data(RadioModel::Role_Url).toUrl();
847
930
if (!song_urls.isEmpty()) {
848
InsertUrls(song_urls, play_now, pos);
931
InsertUrls(song_urls, pos, play_now, enqueue);
851
return InsertItems(playlist_items, pos);
935
InsertItems(playlist_items, pos, play_now, enqueue);
854
938
QMimeData* Playlist::mimeData(const QModelIndexList& indexes) const {
891
975
case Column_Title: strcmp(title);
892
976
case Column_Artist: strcmp(artist);
893
977
case Column_Album: strcmp(album);
894
case Column_Length: cmp(length);
978
case Column_Length: cmp(length_nanosec);
895
979
case Column_Track: cmp(track);
896
980
case Column_Disc: cmp(disc);
897
981
case Column_Year: cmp(year);
1046
1130
watcher->deleteLater();
1048
1132
PlaylistItemList items = watcher->future().results();
1134
// backend returns empty elements for library items which it couldn't
1135
// match (because they got deleted); we don't need those
1136
QMutableListIterator<PlaylistItemPtr> it(items);
1137
while (it.hasNext()) {
1138
PlaylistItemPtr item = it.next();
1140
if (item->IsLocalLibraryItem() && item->Metadata().filename().isEmpty()) {
1049
1145
is_loading_ = true;
1050
1146
InsertItems(items, 0);
1051
1147
is_loading_ = false;
1053
1149
PlaylistBackend::Playlist p = backend_->GetPlaylist(id_);
1055
last_played_item_index_ =
1056
p.last_played == -1 ? QModelIndex() : index(p.last_played);
1151
// the newly loaded list of items might be shorter than it was before so
1152
// look out for a bad last_played index
1153
last_played_item_index_ = p.last_played == -1 || p.last_played >= rowCount()
1155
: index(p.last_played);
1058
1157
if (!p.dynamic_type.isEmpty()) {
1059
1158
GeneratorPtr gen = Generator::Create(p.dynamic_type);
1177
emit RestoreFinished();
1180
s.beginGroup(kSettingsGroup);
1182
// should we gray out deleted songs asynchronously on startup?
1183
if(s.value("greyoutdeleted", false).toBool()) {
1184
QtConcurrent::run(this, &Playlist::InvalidateDeletedSongs);
1188
static bool DescendingIntLessThan(int a, int b) {
1192
void Playlist::RemoveItemsWithoutUndo(const QList<int>& indicesIn) {
1193
// Sort the indices descending because removing elements 'backwards'
1194
// is easier - indices don't 'move' in the process.
1195
QList<int> indices = indicesIn;
1196
qSort(indices.begin(), indices.end(), DescendingIntLessThan);
1198
for(int j = 0; j < indices.count(); j++) {
1199
int beginning = indices[j], end = indices[j];
1201
// Splits the indices into sequences. For example this: [1, 2, 4],
1202
// will get split into [1, 2] and [4].
1203
while(j != indices.count() - 1 && indices[j] == indices[j + 1] + 1) {
1208
// Remove the current sequence.
1209
removeRows(beginning, end - beginning + 1);
1079
1213
bool Playlist::removeRows(int row, int count, const QModelIndex& parent) {
1156
1290
current_item_->SetTemporaryMetadata(song);
1157
1291
UpdateScrobblePoint();
1159
emit dataChanged(index(current_item_index_.row(), 0), index(current_item_index_.row(), ColumnCount-1));
1160
emit CurrentSongChanged(song);
1293
InformOfCurrentSongChange(index(current_item_index_.row(), 0),
1294
index(current_item_index_.row(), ColumnCount-1),
1163
1298
void Playlist::ClearStreamMetadata() {
1259
1400
void Playlist::ReloadItems(const QList<int>& rows) {
1260
1401
foreach (int row, rows) {
1261
item_at(row)->Reload();
1262
emit dataChanged(index(row, 0), index(row, ColumnCount-1));
1402
PlaylistItemPtr item = item_at(row);
1405
InformOfCurrentSongChange(index(row, 0), index(row, ColumnCount-1),
1410
void Playlist::RateSong(const QModelIndex& index, double rating) {
1411
int row = index.row();
1413
if(has_item_at(row)) {
1414
PlaylistItemPtr item = item_at(row);
1415
if (item && item->IsLocalLibraryItem() && item->Metadata().id() != -1) {
1416
library_->UpdateSongRatingAsync(item->Metadata().id(), rating);
1421
void Playlist::AddSongInsertVetoListener(SongInsertVetoListener* listener) {
1422
veto_listeners_.append(listener);
1423
connect(listener, SIGNAL(destroyed()), this, SLOT(SongInsertVetoListenerDestroyed()));
1426
void Playlist::RemoveSongInsertVetoListener(SongInsertVetoListener* listener) {
1427
disconnect(listener, SIGNAL(destroyed()), this, SLOT(SongInsertVetoListenerDestroyed()));
1428
veto_listeners_.removeAll(listener);
1431
void Playlist::SongInsertVetoListenerDestroyed() {
1432
// qobject_cast returns NULL here for Python SIP listeners.
1433
veto_listeners_.removeAll(static_cast<SongInsertVetoListener*>(sender()));
1266
1436
void Playlist::Shuffle() {
1293
1463
void Playlist::ReshuffleIndices() {
1294
1464
if (!is_shuffled_) {
1295
1465
std::sort(virtual_items_.begin(), virtual_items_.end());
1296
if (current_index() != -1)
1297
current_virtual_index_ = virtual_items_.indexOf(current_index());
1466
if (current_row() != -1)
1467
current_virtual_index_ = virtual_items_.indexOf(current_row());
1299
1469
QList<int>::iterator begin = virtual_items_.begin();
1300
1470
if (current_virtual_index_ != -1)
1560
void Playlist::set_column_alignments(const ColumnAlignmentMap& column_alignments) {
1561
column_alignments_ = column_alignments;
1564
void Playlist::set_column_align_left(int column) {
1565
column_alignments_[column] = (Qt::AlignLeft | Qt::AlignVCenter);
1568
void Playlist::set_column_align_center(int column) {
1569
column_alignments_[column] = Qt::AlignCenter;
1572
void Playlist::set_column_align_right(int column) {
1573
column_alignments_[column] = (Qt::AlignRight | Qt::AlignVCenter);
1576
void Playlist::InformOfCurrentSongChange(const QModelIndex& top_left, const QModelIndex& bottom_right,
1577
const Song& metadata) {
1578
emit dataChanged(top_left, bottom_right);
1579
// if the song is invalid, we won't play it - there's no point in
1580
// informing anybody about the change
1581
if(metadata.is_valid()) {
1582
emit CurrentSongChanged(metadata);
1586
void Playlist::InvalidateDeletedSongs() {
1587
QList<int> invalidated_rows;
1589
for (int row = 0; row < items_.count(); ++row) {
1590
PlaylistItemPtr item = items_[row];
1591
Song song = item->Metadata();
1593
if(!song.is_stream()) {
1594
bool exists = QFile::exists(song.filename());
1596
if(!exists && !item->HasForegroundColor(kInvalidSongPriority)) {
1597
// gray out the song if it's not there
1598
item->SetForegroundColor(kInvalidSongPriority, kInvalidSongColor);
1599
invalidated_rows.append(row);
1600
} else if(exists && item->HasForegroundColor(kInvalidSongPriority)) {
1601
item->RemoveForegroundColor(kInvalidSongPriority);
1602
invalidated_rows.append(row);
1607
ReloadItems(invalidated_rows);
1610
bool Playlist::ApplyValidityOnCurrentSong(const QUrl& url, bool valid) {
1611
PlaylistItemPtr current = current_item();
1614
Song current_song = current->Metadata();
1616
// if validity has changed, reload the item
1617
if(!current_song.is_stream() &&
1618
current_song.filename() == url.toLocalFile() &&
1619
current_song.is_valid() != QFile::exists(current_song.filename())) {
1620
ReloadItems(QList<int>() << current_row());
1623
// gray out the song if it's now broken; otherwise undo the gray color
1625
current->RemoveForegroundColor(kInvalidSongPriority);
1627
current->SetForegroundColor(kInvalidSongPriority, kInvalidSongColor);