Formatting
This commit is contained in:
parent
c890af6306
commit
ce8b7303c0
@ -1639,495 +1639,492 @@ void Playlist::StopAfter(int row) {
|
|||||||
else
|
else
|
||||||
stop_after_ = index(row, 0);
|
stop_after_ = index(row, 0);
|
||||||
|
|
||||||
if (old_stop_after.isValid())
|
if (old_stop_after.isValid())
|
||||||
emit dataChanged(
|
emit dataChanged(
|
||||||
old_stop_after,
|
old_stop_after,
|
||||||
old_stop_after.sibling(old_stop_after.row(), ColumnCount - 1));
|
old_stop_after.sibling(old_stop_after.row(), ColumnCount - 1));
|
||||||
if (stop_after_.isValid())
|
if (stop_after_.isValid())
|
||||||
emit dataChanged(stop_after_,
|
emit dataChanged(stop_after_,
|
||||||
stop_after_.sibling(stop_after_.row(), ColumnCount - 1));
|
stop_after_.sibling(stop_after_.row(), ColumnCount - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::SetStreamMetadata(const QUrl& url, const Song& song) {
|
||||||
|
qLog(Debug) << "Setting metadata for" << url << "to" << song.artist()
|
||||||
|
<< song.title();
|
||||||
|
if (!current_item()) return;
|
||||||
|
|
||||||
|
if (current_item()->Url() != url) return;
|
||||||
|
|
||||||
|
// Don't update the metadata if it's only a minor change from before
|
||||||
|
if (current_item()->Metadata().artist() == song.artist() &&
|
||||||
|
current_item()->Metadata().title() == song.title())
|
||||||
|
return;
|
||||||
|
|
||||||
|
current_item()->SetTemporaryMetadata(song);
|
||||||
|
UpdateScrobblePoint();
|
||||||
|
|
||||||
|
InformOfCurrentSongChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::ClearStreamMetadata() {
|
||||||
|
if (!current_item()) return;
|
||||||
|
|
||||||
|
current_item()->ClearTemporaryMetadata();
|
||||||
|
UpdateScrobblePoint();
|
||||||
|
|
||||||
|
emit dataChanged(index(current_item_index_.row(), 0),
|
||||||
|
index(current_item_index_.row(), ColumnCount - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Playlist::stop_after_current() const {
|
||||||
|
return stop_after_.isValid() && current_item_index_.isValid() &&
|
||||||
|
stop_after_.row() == current_item_index_.row();
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaylistItemPtr Playlist::current_item() const {
|
||||||
|
// QList[] runs in constant time, so no need to cache current_item
|
||||||
|
if (current_item_index_.isValid() &&
|
||||||
|
current_item_index_.row() <= items_.length())
|
||||||
|
return items_[current_item_index_.row()];
|
||||||
|
return PlaylistItemPtr();
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaylistItem::Options Playlist::current_item_options() const {
|
||||||
|
if (!current_item()) return PlaylistItem::Default;
|
||||||
|
|
||||||
|
return current_item()->options();
|
||||||
|
}
|
||||||
|
|
||||||
|
Song Playlist::current_item_metadata() const {
|
||||||
|
if (!current_item()) return Song();
|
||||||
|
|
||||||
|
return current_item()->Metadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::UpdateScrobblePoint() {
|
||||||
|
const qint64 length = current_item_metadata().length_nanosec();
|
||||||
|
|
||||||
|
if (length == 0) {
|
||||||
|
scrobble_point_ = 240ll * kNsecPerSec; // 4 minutes
|
||||||
|
} else {
|
||||||
|
scrobble_point_ =
|
||||||
|
qBound(31ll * kNsecPerSec, length / 2, 240ll * kNsecPerSec);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Playlist::SetStreamMetadata(const QUrl& url, const Song& song) {
|
set_lastfm_status(LastFM_New);
|
||||||
qLog(Debug) << "Setting metadata for" << url << "to" << song.artist()
|
have_incremented_playcount_ = false;
|
||||||
<< song.title();
|
}
|
||||||
if (!current_item()) return;
|
|
||||||
|
|
||||||
if (current_item()->Url() != url) return;
|
void Playlist::Clear() {
|
||||||
|
const int count = items_.count();
|
||||||
|
|
||||||
// Don't update the metadata if it's only a minor change from before
|
if (count > kUndoItemLimit) {
|
||||||
if (current_item()->Metadata().artist() == song.artist() &&
|
// Too big to keep in the undo stack. Also clear the stack because it
|
||||||
current_item()->Metadata().title() == song.title())
|
// might have been invalidated.
|
||||||
return;
|
RemoveItemsWithoutUndo(0, count);
|
||||||
|
undo_stack_->clear();
|
||||||
current_item()->SetTemporaryMetadata(song);
|
} else {
|
||||||
UpdateScrobblePoint();
|
undo_stack_->push(new PlaylistUndoCommands::RemoveItems(this, 0, count));
|
||||||
|
|
||||||
InformOfCurrentSongChange();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Playlist::ClearStreamMetadata() {
|
TurnOffDynamicPlaylist();
|
||||||
if (!current_item()) return;
|
|
||||||
|
|
||||||
current_item()->ClearTemporaryMetadata();
|
Save();
|
||||||
UpdateScrobblePoint();
|
}
|
||||||
|
|
||||||
emit dataChanged(index(current_item_index_.row(), 0),
|
void Playlist::TurnOffDynamicPlaylist() {
|
||||||
index(current_item_index_.row(), ColumnCount - 1));
|
dynamic_playlist_.reset();
|
||||||
|
|
||||||
|
if (playlist_sequence_) {
|
||||||
|
playlist_sequence_->SetUsingDynamicPlaylist(false);
|
||||||
|
ShuffleModeChanged(playlist_sequence_->shuffle_mode());
|
||||||
|
}
|
||||||
|
emit DynamicModeChanged(false);
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::RepopulateDynamicPlaylist() {
|
||||||
|
if (!dynamic_playlist_) return;
|
||||||
|
|
||||||
|
RemoveItemsNotInQueue();
|
||||||
|
InsertSmartPlaylist(dynamic_playlist_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::ExpandDynamicPlaylist() {
|
||||||
|
if (!dynamic_playlist_) return;
|
||||||
|
|
||||||
|
InsertDynamicItems(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::RemoveItemsNotInQueue() {
|
||||||
|
if (queue_->is_empty()) {
|
||||||
|
RemoveItemsWithoutUndo(0, items_.count());
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Playlist::stop_after_current() const {
|
int start = 0;
|
||||||
return stop_after_.isValid() && current_item_index_.isValid() &&
|
forever {
|
||||||
stop_after_.row() == current_item_index_.row();
|
// Find a place to start - first row that isn't in the queue
|
||||||
}
|
|
||||||
|
|
||||||
PlaylistItemPtr Playlist::current_item() const {
|
|
||||||
// QList[] runs in constant time, so no need to cache current_item
|
|
||||||
if (current_item_index_.isValid() &&
|
|
||||||
current_item_index_.row() <= items_.length())
|
|
||||||
return items_[current_item_index_.row()];
|
|
||||||
return PlaylistItemPtr();
|
|
||||||
}
|
|
||||||
|
|
||||||
PlaylistItem::Options Playlist::current_item_options() const {
|
|
||||||
if (!current_item()) return PlaylistItem::Default;
|
|
||||||
|
|
||||||
return current_item()->options();
|
|
||||||
}
|
|
||||||
|
|
||||||
Song Playlist::current_item_metadata() const {
|
|
||||||
if (!current_item()) return Song();
|
|
||||||
|
|
||||||
return current_item()->Metadata();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Playlist::UpdateScrobblePoint() {
|
|
||||||
const qint64 length = current_item_metadata().length_nanosec();
|
|
||||||
|
|
||||||
if (length == 0) {
|
|
||||||
scrobble_point_ = 240ll * kNsecPerSec; // 4 minutes
|
|
||||||
} else {
|
|
||||||
scrobble_point_ =
|
|
||||||
qBound(31ll * kNsecPerSec, length / 2, 240ll * kNsecPerSec);
|
|
||||||
}
|
|
||||||
|
|
||||||
set_lastfm_status(LastFM_New);
|
|
||||||
have_incremented_playcount_ = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Playlist::Clear() {
|
|
||||||
const int count = items_.count();
|
|
||||||
|
|
||||||
if (count > kUndoItemLimit) {
|
|
||||||
// Too big to keep in the undo stack. Also clear the stack because it
|
|
||||||
// might have been invalidated.
|
|
||||||
RemoveItemsWithoutUndo(0, count);
|
|
||||||
undo_stack_->clear();
|
|
||||||
} else {
|
|
||||||
undo_stack_->push(new PlaylistUndoCommands::RemoveItems(this, 0, count));
|
|
||||||
}
|
|
||||||
|
|
||||||
TurnOffDynamicPlaylist();
|
|
||||||
|
|
||||||
Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Playlist::TurnOffDynamicPlaylist() {
|
|
||||||
dynamic_playlist_.reset();
|
|
||||||
|
|
||||||
if (playlist_sequence_) {
|
|
||||||
playlist_sequence_->SetUsingDynamicPlaylist(false);
|
|
||||||
ShuffleModeChanged(playlist_sequence_->shuffle_mode());
|
|
||||||
}
|
|
||||||
emit DynamicModeChanged(false);
|
|
||||||
Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Playlist::RepopulateDynamicPlaylist() {
|
|
||||||
if (!dynamic_playlist_) return;
|
|
||||||
|
|
||||||
RemoveItemsNotInQueue();
|
|
||||||
InsertSmartPlaylist(dynamic_playlist_);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Playlist::ExpandDynamicPlaylist() {
|
|
||||||
if (!dynamic_playlist_) return;
|
|
||||||
|
|
||||||
InsertDynamicItems(5);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Playlist::RemoveItemsNotInQueue() {
|
|
||||||
if (queue_->is_empty()) {
|
|
||||||
RemoveItemsWithoutUndo(0, items_.count());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int start = 0;
|
|
||||||
forever {
|
forever {
|
||||||
// Find a place to start - first row that isn't in the queue
|
if (start >= rowCount()) return;
|
||||||
forever {
|
if (!queue_->ContainsSourceRow(start)) break;
|
||||||
if (start >= rowCount()) return;
|
|
||||||
if (!queue_->ContainsSourceRow(start)) break;
|
|
||||||
start++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Figure out how many rows to remove - keep going until we find a row
|
|
||||||
// that is in the queue
|
|
||||||
int count = 1;
|
|
||||||
forever {
|
|
||||||
if (start + count >= rowCount()) break;
|
|
||||||
if (queue_->ContainsSourceRow(start + count)) break;
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoveItemsWithoutUndo(start, count);
|
|
||||||
start++;
|
start++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Figure out how many rows to remove - keep going until we find a row
|
||||||
|
// that is in the queue
|
||||||
|
int count = 1;
|
||||||
|
forever {
|
||||||
|
if (start + count >= rowCount()) break;
|
||||||
|
if (queue_->ContainsSourceRow(start + count)) break;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoveItemsWithoutUndo(start, count);
|
||||||
|
start++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::ReloadItems(const QList<int>& rows) {
|
||||||
|
for (int row : rows) {
|
||||||
|
PlaylistItemPtr item = item_at(row);
|
||||||
|
|
||||||
|
item->Reload();
|
||||||
|
|
||||||
|
if (row == current_row()) {
|
||||||
|
InformOfCurrentSongChange();
|
||||||
|
} else {
|
||||||
|
emit dataChanged(index(row, 0), index(row, ColumnCount - 1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Playlist::ReloadItems(const QList<int>& rows) {
|
Save();
|
||||||
for (int row : rows) {
|
}
|
||||||
PlaylistItemPtr item = item_at(row);
|
|
||||||
|
|
||||||
item->Reload();
|
void Playlist::RateSong(const QModelIndex& index, double rating) {
|
||||||
|
int row = index.row();
|
||||||
|
|
||||||
if (row == current_row()) {
|
if (has_item_at(row)) {
|
||||||
InformOfCurrentSongChange();
|
PlaylistItemPtr item = item_at(row);
|
||||||
|
if (item && item->IsLocalLibraryItem() && item->Metadata().id() != -1) {
|
||||||
|
library_->UpdateSongRatingAsync(item->Metadata().id(), rating);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::AddSongInsertVetoListener(SongInsertVetoListener* listener) {
|
||||||
|
veto_listeners_.append(listener);
|
||||||
|
connect(listener, SIGNAL(destroyed()), this,
|
||||||
|
SLOT(SongInsertVetoListenerDestroyed()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::RemoveSongInsertVetoListener(SongInsertVetoListener* listener) {
|
||||||
|
disconnect(listener, SIGNAL(destroyed()), this,
|
||||||
|
SLOT(SongInsertVetoListenerDestroyed()));
|
||||||
|
veto_listeners_.removeAll(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::SongInsertVetoListenerDestroyed() {
|
||||||
|
veto_listeners_.removeAll(qobject_cast<SongInsertVetoListener*>(sender()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::Shuffle() {
|
||||||
|
PlaylistItemList new_items(items_);
|
||||||
|
|
||||||
|
int begin = 0;
|
||||||
|
if (dynamic_playlist_ && current_item_index_.isValid())
|
||||||
|
begin += current_item_index_.row() + 1;
|
||||||
|
|
||||||
|
const int count = items_.count();
|
||||||
|
for (int i = begin; i < count; ++i) {
|
||||||
|
int new_pos = i + (rand() % (count - i));
|
||||||
|
|
||||||
|
std::swap(new_items[i], new_items[new_pos]);
|
||||||
|
}
|
||||||
|
|
||||||
|
undo_stack_->push(new PlaylistUndoCommands::ShuffleItems(this, new_items));
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
bool AlbumShuffleComparator(const QMap<QString, int>& album_key_positions,
|
||||||
|
const QMap<int, QString>& album_keys, int left,
|
||||||
|
int right) {
|
||||||
|
const int left_pos = album_key_positions[album_keys[left]];
|
||||||
|
const int right_pos = album_key_positions[album_keys[right]];
|
||||||
|
|
||||||
|
if (left_pos == right_pos) return left < right;
|
||||||
|
return left_pos < right_pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::ReshuffleIndices() {
|
||||||
|
if (!playlist_sequence_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playlist_sequence_->shuffle_mode() == PlaylistSequence::Shuffle_Off) {
|
||||||
|
// No shuffling - sort the virtual item list normally.
|
||||||
|
std::sort(virtual_items_.begin(), virtual_items_.end());
|
||||||
|
if (current_row() != -1)
|
||||||
|
current_virtual_index_ = virtual_items_.indexOf(current_row());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user is already playing a song, advance the begin iterator to
|
||||||
|
// only shuffle items that haven't been played yet.
|
||||||
|
QList<int>::iterator begin = virtual_items_.begin();
|
||||||
|
QList<int>::iterator end = virtual_items_.end();
|
||||||
|
if (current_virtual_index_ != -1)
|
||||||
|
std::advance(begin, current_virtual_index_ + 1);
|
||||||
|
|
||||||
|
switch (playlist_sequence_->shuffle_mode()) {
|
||||||
|
case PlaylistSequence::Shuffle_Off:
|
||||||
|
// Handled above.
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlaylistSequence::Shuffle_All:
|
||||||
|
case PlaylistSequence::Shuffle_InsideAlbum:
|
||||||
|
std::random_shuffle(begin, end);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case PlaylistSequence::Shuffle_Albums: {
|
||||||
|
QMap<int, QString> album_keys; // real index -> key
|
||||||
|
QSet<QString> album_key_set; // unique keys
|
||||||
|
|
||||||
|
// Find all the unique albums in the playlist
|
||||||
|
for (QList<int>::iterator it = begin; it != end; ++it) {
|
||||||
|
const int index = *it;
|
||||||
|
const QString key = items_[index]->Metadata().AlbumKey();
|
||||||
|
album_keys[index] = key;
|
||||||
|
album_key_set << key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shuffle them
|
||||||
|
QStringList shuffled_album_keys = album_key_set.toList();
|
||||||
|
std::random_shuffle(shuffled_album_keys.begin(),
|
||||||
|
shuffled_album_keys.end());
|
||||||
|
|
||||||
|
// If the user is currently playing a song, force its album to be first.
|
||||||
|
if (current_virtual_index_ != -1) {
|
||||||
|
const QString key = items_[current_row()]->Metadata().AlbumKey();
|
||||||
|
const int pos = shuffled_album_keys.indexOf(key);
|
||||||
|
if (pos >= 1) {
|
||||||
|
std::swap(shuffled_album_keys[0], shuffled_album_keys[pos]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create album key -> position mapping for fast lookup
|
||||||
|
QMap<QString, int> album_key_positions;
|
||||||
|
for (int i = 0; i < shuffled_album_keys.count(); ++i) {
|
||||||
|
album_key_positions[shuffled_album_keys[i]] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the virtual items
|
||||||
|
std::stable_sort(begin, end,
|
||||||
|
std::bind(AlbumShuffleComparator, album_key_positions,
|
||||||
|
album_keys, _1, _2));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::set_sequence(PlaylistSequence* v) {
|
||||||
|
playlist_sequence_ = v;
|
||||||
|
connect(v, SIGNAL(ShuffleModeChanged(PlaylistSequence::ShuffleMode)),
|
||||||
|
SLOT(ShuffleModeChanged(PlaylistSequence::ShuffleMode)));
|
||||||
|
|
||||||
|
ShuffleModeChanged(v->shuffle_mode());
|
||||||
|
}
|
||||||
|
|
||||||
|
QSortFilterProxyModel* Playlist::proxy() const { return proxy_; }
|
||||||
|
|
||||||
|
SongList Playlist::GetAllSongs() const {
|
||||||
|
SongList ret;
|
||||||
|
for (PlaylistItemPtr item : items_) {
|
||||||
|
ret << item->Metadata();
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaylistItemList Playlist::GetAllItems() const { return items_; }
|
||||||
|
|
||||||
|
quint64 Playlist::GetTotalLength() const {
|
||||||
|
quint64 ret = 0;
|
||||||
|
for (PlaylistItemPtr item : items_) {
|
||||||
|
quint64 length = item->Metadata().length_nanosec();
|
||||||
|
if (length > 0) ret += length;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaylistItemList Playlist::library_items_by_id(int id) const {
|
||||||
|
return library_items_by_id_.values(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::TracksAboutToBeDequeued(const QModelIndex&, int begin, int end) {
|
||||||
|
for (int i = begin; i <= end; ++i) {
|
||||||
|
temp_dequeue_change_indexes_
|
||||||
|
<< queue_->mapToSource(queue_->index(i, Column_Title));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::TracksDequeued() {
|
||||||
|
for (const QModelIndex& index : temp_dequeue_change_indexes_) {
|
||||||
|
emit dataChanged(index, index);
|
||||||
|
}
|
||||||
|
temp_dequeue_change_indexes_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::TracksEnqueued(const QModelIndex&, int begin, int end) {
|
||||||
|
const QModelIndex& b =
|
||||||
|
queue_->mapToSource(queue_->index(begin, Column_Title));
|
||||||
|
const QModelIndex& e = queue_->mapToSource(queue_->index(end, Column_Title));
|
||||||
|
emit dataChanged(b, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::QueueLayoutChanged() {
|
||||||
|
for (int i = 0; i < queue_->rowCount(); ++i) {
|
||||||
|
const QModelIndex& index =
|
||||||
|
queue_->mapToSource(queue_->index(i, Column_Title));
|
||||||
|
emit dataChanged(index, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::ItemChanged(PlaylistItemPtr item) {
|
||||||
|
for (int row = 0; row < items_.count(); ++row) {
|
||||||
|
if (items_[row] == item) {
|
||||||
|
emit dataChanged(index(row, 0), index(row, ColumnCount - 1));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::InformOfCurrentSongChange() {
|
||||||
|
emit dataChanged(index(current_item_index_.row(), 0),
|
||||||
|
index(current_item_index_.row(), ColumnCount - 1));
|
||||||
|
|
||||||
|
// if the song is invalid, we won't play it - there's no point in
|
||||||
|
// informing anybody about the change
|
||||||
|
const Song metadata(current_item_metadata());
|
||||||
|
if (metadata.is_valid()) {
|
||||||
|
emit CurrentSongChanged(metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::InvalidateDeletedSongs() {
|
||||||
|
QList<int> invalidated_rows;
|
||||||
|
|
||||||
|
for (int row = 0; row < items_.count(); ++row) {
|
||||||
|
PlaylistItemPtr item = items_[row];
|
||||||
|
Song song = item->Metadata();
|
||||||
|
|
||||||
|
if (!song.is_stream()) {
|
||||||
|
bool exists = QFile::exists(song.url().toLocalFile());
|
||||||
|
|
||||||
|
if (!exists && !item->HasForegroundColor(kInvalidSongPriority)) {
|
||||||
|
// gray out the song if it's not there
|
||||||
|
item->SetForegroundColor(kInvalidSongPriority, kInvalidSongColor);
|
||||||
|
invalidated_rows.append(row);
|
||||||
|
} else if (exists && item->HasForegroundColor(kInvalidSongPriority)) {
|
||||||
|
item->RemoveForegroundColor(kInvalidSongPriority);
|
||||||
|
invalidated_rows.append(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ReloadItems(invalidated_rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::RemoveDeletedSongs() {
|
||||||
|
QList<int> rows_to_remove;
|
||||||
|
|
||||||
|
for (int row = 0; row < items_.count(); ++row) {
|
||||||
|
PlaylistItemPtr item = items_[row];
|
||||||
|
Song song = item->Metadata();
|
||||||
|
|
||||||
|
if (!song.is_stream() && !QFile::exists(song.url().toLocalFile())) {
|
||||||
|
rows_to_remove.append(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeRows(rows_to_remove);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SongSimilarHash {
|
||||||
|
long operator()(const Song& song) const { return HashSimilar(song); }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SongSimilarEqual {
|
||||||
|
long operator()(const Song& song1, const Song& song2) const {
|
||||||
|
return song1.IsSimilar(song2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void Playlist::RemoveDuplicateSongs() {
|
||||||
|
QList<int> rows_to_remove;
|
||||||
|
unordered_map<Song, int, SongSimilarHash, SongSimilarEqual> unique_songs;
|
||||||
|
|
||||||
|
for (int row = 0; row < items_.count(); ++row) {
|
||||||
|
PlaylistItemPtr item = items_[row];
|
||||||
|
const Song& song = item->Metadata();
|
||||||
|
|
||||||
|
bool found_duplicate = false;
|
||||||
|
|
||||||
|
auto uniq_song_it = unique_songs.find(song);
|
||||||
|
if (uniq_song_it != unique_songs.end()) {
|
||||||
|
const Song& uniq_song = uniq_song_it->first;
|
||||||
|
|
||||||
|
if (song.bitrate() > uniq_song.bitrate()) {
|
||||||
|
rows_to_remove.append(unique_songs[uniq_song]);
|
||||||
|
unique_songs.erase(uniq_song);
|
||||||
|
unique_songs.insert(std::make_pair(song, row));
|
||||||
} else {
|
} else {
|
||||||
emit dataChanged(index(row, 0), index(row, ColumnCount - 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Playlist::RateSong(const QModelIndex& index, double rating) {
|
|
||||||
int row = index.row();
|
|
||||||
|
|
||||||
if (has_item_at(row)) {
|
|
||||||
PlaylistItemPtr item = item_at(row);
|
|
||||||
if (item && item->IsLocalLibraryItem() && item->Metadata().id() != -1) {
|
|
||||||
library_->UpdateSongRatingAsync(item->Metadata().id(), rating);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Playlist::AddSongInsertVetoListener(SongInsertVetoListener * listener) {
|
|
||||||
veto_listeners_.append(listener);
|
|
||||||
connect(listener, SIGNAL(destroyed()), this,
|
|
||||||
SLOT(SongInsertVetoListenerDestroyed()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void Playlist::RemoveSongInsertVetoListener(SongInsertVetoListener *
|
|
||||||
listener) {
|
|
||||||
disconnect(listener, SIGNAL(destroyed()), this,
|
|
||||||
SLOT(SongInsertVetoListenerDestroyed()));
|
|
||||||
veto_listeners_.removeAll(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Playlist::SongInsertVetoListenerDestroyed() {
|
|
||||||
veto_listeners_.removeAll(qobject_cast<SongInsertVetoListener*>(sender()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void Playlist::Shuffle() {
|
|
||||||
PlaylistItemList new_items(items_);
|
|
||||||
|
|
||||||
int begin = 0;
|
|
||||||
if (dynamic_playlist_ && current_item_index_.isValid())
|
|
||||||
begin += current_item_index_.row() + 1;
|
|
||||||
|
|
||||||
const int count = items_.count();
|
|
||||||
for (int i = begin; i < count; ++i) {
|
|
||||||
int new_pos = i + (rand() % (count - i));
|
|
||||||
|
|
||||||
std::swap(new_items[i], new_items[new_pos]);
|
|
||||||
}
|
|
||||||
|
|
||||||
undo_stack_->push(new PlaylistUndoCommands::ShuffleItems(this, new_items));
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
bool AlbumShuffleComparator(const QMap<QString, int>& album_key_positions,
|
|
||||||
const QMap<int, QString>& album_keys, int left,
|
|
||||||
int right) {
|
|
||||||
const int left_pos = album_key_positions[album_keys[left]];
|
|
||||||
const int right_pos = album_key_positions[album_keys[right]];
|
|
||||||
|
|
||||||
if (left_pos == right_pos) return left < right;
|
|
||||||
return left_pos < right_pos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Playlist::ReshuffleIndices() {
|
|
||||||
if (!playlist_sequence_) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (playlist_sequence_->shuffle_mode() == PlaylistSequence::Shuffle_Off) {
|
|
||||||
// No shuffling - sort the virtual item list normally.
|
|
||||||
std::sort(virtual_items_.begin(), virtual_items_.end());
|
|
||||||
if (current_row() != -1)
|
|
||||||
current_virtual_index_ = virtual_items_.indexOf(current_row());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the user is already playing a song, advance the begin iterator to
|
|
||||||
// only shuffle items that haven't been played yet.
|
|
||||||
QList<int>::iterator begin = virtual_items_.begin();
|
|
||||||
QList<int>::iterator end = virtual_items_.end();
|
|
||||||
if (current_virtual_index_ != -1)
|
|
||||||
std::advance(begin, current_virtual_index_ + 1);
|
|
||||||
|
|
||||||
switch (playlist_sequence_->shuffle_mode()) {
|
|
||||||
case PlaylistSequence::Shuffle_Off:
|
|
||||||
// Handled above.
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PlaylistSequence::Shuffle_All:
|
|
||||||
case PlaylistSequence::Shuffle_InsideAlbum:
|
|
||||||
std::random_shuffle(begin, end);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PlaylistSequence::Shuffle_Albums: {
|
|
||||||
QMap<int, QString> album_keys; // real index -> key
|
|
||||||
QSet<QString> album_key_set; // unique keys
|
|
||||||
|
|
||||||
// Find all the unique albums in the playlist
|
|
||||||
for (QList<int>::iterator it = begin; it != end; ++it) {
|
|
||||||
const int index = *it;
|
|
||||||
const QString key = items_[index]->Metadata().AlbumKey();
|
|
||||||
album_keys[index] = key;
|
|
||||||
album_key_set << key;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shuffle them
|
|
||||||
QStringList shuffled_album_keys = album_key_set.toList();
|
|
||||||
std::random_shuffle(shuffled_album_keys.begin(),
|
|
||||||
shuffled_album_keys.end());
|
|
||||||
|
|
||||||
// If the user is currently playing a song, force its album to be first.
|
|
||||||
if (current_virtual_index_ != -1) {
|
|
||||||
const QString key = items_[current_row()]->Metadata().AlbumKey();
|
|
||||||
const int pos = shuffled_album_keys.indexOf(key);
|
|
||||||
if (pos >= 1) {
|
|
||||||
std::swap(shuffled_album_keys[0], shuffled_album_keys[pos]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create album key -> position mapping for fast lookup
|
|
||||||
QMap<QString, int> album_key_positions;
|
|
||||||
for (int i = 0; i < shuffled_album_keys.count(); ++i) {
|
|
||||||
album_key_positions[shuffled_album_keys[i]] = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort the virtual items
|
|
||||||
std::stable_sort(begin, end,
|
|
||||||
std::bind(AlbumShuffleComparator, album_key_positions,
|
|
||||||
album_keys, _1, _2));
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Playlist::set_sequence(PlaylistSequence * v) {
|
|
||||||
playlist_sequence_ = v;
|
|
||||||
connect(v, SIGNAL(ShuffleModeChanged(PlaylistSequence::ShuffleMode)),
|
|
||||||
SLOT(ShuffleModeChanged(PlaylistSequence::ShuffleMode)));
|
|
||||||
|
|
||||||
ShuffleModeChanged(v->shuffle_mode());
|
|
||||||
}
|
|
||||||
|
|
||||||
QSortFilterProxyModel* Playlist::proxy() const { return proxy_; }
|
|
||||||
|
|
||||||
SongList Playlist::GetAllSongs() const {
|
|
||||||
SongList ret;
|
|
||||||
for (PlaylistItemPtr item : items_) {
|
|
||||||
ret << item->Metadata();
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
PlaylistItemList Playlist::GetAllItems() const { return items_; }
|
|
||||||
|
|
||||||
quint64 Playlist::GetTotalLength() const {
|
|
||||||
quint64 ret = 0;
|
|
||||||
for (PlaylistItemPtr item : items_) {
|
|
||||||
quint64 length = item->Metadata().length_nanosec();
|
|
||||||
if (length > 0) ret += length;
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
PlaylistItemList Playlist::library_items_by_id(int id) const {
|
|
||||||
return library_items_by_id_.values(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Playlist::TracksAboutToBeDequeued(const QModelIndex&, int begin,
|
|
||||||
int end) {
|
|
||||||
for (int i = begin; i <= end; ++i) {
|
|
||||||
temp_dequeue_change_indexes_
|
|
||||||
<< queue_->mapToSource(queue_->index(i, Column_Title));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Playlist::TracksDequeued() {
|
|
||||||
for (const QModelIndex& index : temp_dequeue_change_indexes_) {
|
|
||||||
emit dataChanged(index, index);
|
|
||||||
}
|
|
||||||
temp_dequeue_change_indexes_.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Playlist::TracksEnqueued(const QModelIndex&, int begin, int end) {
|
|
||||||
const QModelIndex& b =
|
|
||||||
queue_->mapToSource(queue_->index(begin, Column_Title));
|
|
||||||
const QModelIndex& e =
|
|
||||||
queue_->mapToSource(queue_->index(end, Column_Title));
|
|
||||||
emit dataChanged(b, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Playlist::QueueLayoutChanged() {
|
|
||||||
for (int i = 0; i < queue_->rowCount(); ++i) {
|
|
||||||
const QModelIndex& index =
|
|
||||||
queue_->mapToSource(queue_->index(i, Column_Title));
|
|
||||||
emit dataChanged(index, index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Playlist::ItemChanged(PlaylistItemPtr item) {
|
|
||||||
for (int row = 0; row < items_.count(); ++row) {
|
|
||||||
if (items_[row] == item) {
|
|
||||||
emit dataChanged(index(row, 0), index(row, ColumnCount - 1));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Playlist::InformOfCurrentSongChange() {
|
|
||||||
emit dataChanged(index(current_item_index_.row(), 0),
|
|
||||||
index(current_item_index_.row(), ColumnCount - 1));
|
|
||||||
|
|
||||||
// if the song is invalid, we won't play it - there's no point in
|
|
||||||
// informing anybody about the change
|
|
||||||
const Song metadata(current_item_metadata());
|
|
||||||
if (metadata.is_valid()) {
|
|
||||||
emit CurrentSongChanged(metadata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Playlist::InvalidateDeletedSongs() {
|
|
||||||
QList<int> invalidated_rows;
|
|
||||||
|
|
||||||
for (int row = 0; row < items_.count(); ++row) {
|
|
||||||
PlaylistItemPtr item = items_[row];
|
|
||||||
Song song = item->Metadata();
|
|
||||||
|
|
||||||
if (!song.is_stream()) {
|
|
||||||
bool exists = QFile::exists(song.url().toLocalFile());
|
|
||||||
|
|
||||||
if (!exists && !item->HasForegroundColor(kInvalidSongPriority)) {
|
|
||||||
// gray out the song if it's not there
|
|
||||||
item->SetForegroundColor(kInvalidSongPriority, kInvalidSongColor);
|
|
||||||
invalidated_rows.append(row);
|
|
||||||
} else if (exists && item->HasForegroundColor(kInvalidSongPriority)) {
|
|
||||||
item->RemoveForegroundColor(kInvalidSongPriority);
|
|
||||||
invalidated_rows.append(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ReloadItems(invalidated_rows);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Playlist::RemoveDeletedSongs() {
|
|
||||||
QList<int> rows_to_remove;
|
|
||||||
|
|
||||||
for (int row = 0; row < items_.count(); ++row) {
|
|
||||||
PlaylistItemPtr item = items_[row];
|
|
||||||
Song song = item->Metadata();
|
|
||||||
|
|
||||||
if (!song.is_stream() && !QFile::exists(song.url().toLocalFile())) {
|
|
||||||
rows_to_remove.append(row);
|
rows_to_remove.append(row);
|
||||||
}
|
}
|
||||||
|
found_duplicate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeRows(rows_to_remove);
|
if (!found_duplicate) {
|
||||||
}
|
unique_songs.insert(std::make_pair(song, row));
|
||||||
|
|
||||||
struct SongSimilarHash {
|
|
||||||
long operator()(const Song& song) const { return HashSimilar(song); }
|
|
||||||
};
|
|
||||||
|
|
||||||
struct SongSimilarEqual {
|
|
||||||
long operator()(const Song& song1, const Song& song2) const {
|
|
||||||
return song1.IsSimilar(song2);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
void Playlist::RemoveDuplicateSongs() {
|
|
||||||
QList<int> rows_to_remove;
|
|
||||||
unordered_map<Song, int, SongSimilarHash, SongSimilarEqual> unique_songs;
|
|
||||||
|
|
||||||
for (int row = 0; row < items_.count(); ++row) {
|
|
||||||
PlaylistItemPtr item = items_[row];
|
|
||||||
const Song& song = item->Metadata();
|
|
||||||
|
|
||||||
bool found_duplicate = false;
|
|
||||||
|
|
||||||
auto uniq_song_it = unique_songs.find(song);
|
|
||||||
if (uniq_song_it != unique_songs.end()) {
|
|
||||||
const Song& uniq_song = uniq_song_it->first;
|
|
||||||
|
|
||||||
if (song.bitrate() > uniq_song.bitrate()) {
|
|
||||||
rows_to_remove.append(unique_songs[uniq_song]);
|
|
||||||
unique_songs.erase(uniq_song);
|
|
||||||
unique_songs.insert(std::make_pair(song, row));
|
|
||||||
} else {
|
|
||||||
rows_to_remove.append(row);
|
|
||||||
}
|
|
||||||
found_duplicate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found_duplicate) {
|
|
||||||
unique_songs.insert(std::make_pair(song, row));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeRows(rows_to_remove);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Playlist::ApplyValidityOnCurrentSong(const QUrl& url, bool valid) {
|
|
||||||
PlaylistItemPtr current = current_item();
|
|
||||||
|
|
||||||
if (current) {
|
|
||||||
Song current_song = current->Metadata();
|
|
||||||
|
|
||||||
// if validity has changed, reload the item
|
|
||||||
if (!current_song.is_stream() && !current_song.is_cdda() &&
|
|
||||||
current_song.url() == url &&
|
|
||||||
current_song.is_valid() !=
|
|
||||||
QFile::exists(current_song.url().toLocalFile())) {
|
|
||||||
ReloadItems(QList<int>() << current_row());
|
|
||||||
}
|
|
||||||
|
|
||||||
// gray out the song if it's now broken; otherwise undo the gray color
|
|
||||||
if (valid) {
|
|
||||||
current->RemoveForegroundColor(kInvalidSongPriority);
|
|
||||||
} else {
|
|
||||||
current->SetForegroundColor(kInvalidSongPriority, kInvalidSongColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return static_cast<bool>(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Playlist::SetColumnAlignment(const ColumnAlignmentMap& alignment) {
|
|
||||||
column_alignments_ = alignment;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Playlist::SkipTracks(const QModelIndexList& source_indexes) {
|
|
||||||
for (const QModelIndex& source_index : source_indexes) {
|
|
||||||
PlaylistItemPtr track_to_skip = item_at(source_index.row());
|
|
||||||
track_to_skip->SetShouldSkip(!((track_to_skip)->GetShouldSkip()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeRows(rows_to_remove);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Playlist::ApplyValidityOnCurrentSong(const QUrl& url, bool valid) {
|
||||||
|
PlaylistItemPtr current = current_item();
|
||||||
|
|
||||||
|
if (current) {
|
||||||
|
Song current_song = current->Metadata();
|
||||||
|
|
||||||
|
// if validity has changed, reload the item
|
||||||
|
if (!current_song.is_stream() && !current_song.is_cdda() &&
|
||||||
|
current_song.url() == url &&
|
||||||
|
current_song.is_valid() !=
|
||||||
|
QFile::exists(current_song.url().toLocalFile())) {
|
||||||
|
ReloadItems(QList<int>() << current_row());
|
||||||
|
}
|
||||||
|
|
||||||
|
// gray out the song if it's now broken; otherwise undo the gray color
|
||||||
|
if (valid) {
|
||||||
|
current->RemoveForegroundColor(kInvalidSongPriority);
|
||||||
|
} else {
|
||||||
|
current->SetForegroundColor(kInvalidSongPriority, kInvalidSongColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return static_cast<bool>(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::SetColumnAlignment(const ColumnAlignmentMap& alignment) {
|
||||||
|
column_alignments_ = alignment;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playlist::SkipTracks(const QModelIndexList& source_indexes) {
|
||||||
|
for (const QModelIndex& source_index : source_indexes) {
|
||||||
|
PlaylistItemPtr track_to_skip = item_at(source_index.row());
|
||||||
|
track_to_skip->SetShouldSkip(!((track_to_skip)->GetShouldSkip()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user