/* * Strawberry Music Player * This file was part of Clementine. * Copyright 2010, David Sansome * * Strawberry is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Strawberry is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Strawberry. If not, see . * */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "core/application.h" #include "core/closure.h" #include "core/logging.h" #include "core/mimedata.h" #include "core/tagreaderclient.h" #include "collection/collection.h" #include "collection/collectionbackend.h" #include "collection/collectionplaylistitem.h" #include "playlist.h" #include "playlistitem.h" #include "playlistview.h" #include "playlistsequence.h" #include "playlistbackend.h" #include "playlistfilter.h" #include "playlistitemmimedata.h" #include "playlistundocommands.h" #include "queue.h" #include "songloaderinserter.h" #include "songmimedata.h" #include "songplaylistitem.h" #include "tagreadermessages.pb.h" using std::placeholders::_1; using std::placeholders::_2; using std::shared_ptr; using std::unordered_map; const char *Playlist::kCddaMimeType = "x-content/audio-cdda"; const char *Playlist::kRowsMimetype = "application/x-strawberry-playlist-rows"; const char *Playlist::kPlayNowMimetype = "application/x-strawberry-play-now"; const int Playlist::kInvalidSongPriority = 200; const QRgb Playlist::kInvalidSongColor = qRgb(0xC0, 0xC0, 0xC0); const int Playlist::kDynamicHistoryPriority = 100; const QRgb Playlist::kDynamicHistoryColor = qRgb(0x80, 0x80, 0x80); const char *Playlist::kSettingsGroup = "Playlist"; const char *Playlist::kPathType = "path_type"; const char *Playlist::kWriteMetadata = "write_metadata"; const int Playlist::kUndoStackSize = 20; const int Playlist::kUndoItemLimit = 500; Playlist::Playlist(PlaylistBackend *backend, TaskManager *task_manager, CollectionBackend *collection, int id, const QString &special_type, bool favorite, QObject *parent) : QAbstractListModel(parent), is_loading_(false), proxy_(new PlaylistFilter(this)), queue_(new Queue(this)), backend_(backend), task_manager_(task_manager), collection_(collection), id_(id), favorite_(favorite), current_is_paused_(false), current_virtual_index_(-1), is_shuffled_(false), playlist_sequence_(nullptr), ignore_sorting_(false), undo_stack_(new QUndoStack(this)), special_type_(special_type), cancel_restore_(false) { undo_stack_->setUndoLimit(kUndoStackSize); connect(this, SIGNAL(rowsInserted(const QModelIndex&, int, int)), SIGNAL(PlaylistChanged())); connect(this, SIGNAL(rowsRemoved(const QModelIndex&, int, int)), SIGNAL(PlaylistChanged())); Restore(); proxy_->setSourceModel(this); queue_->setSourceModel(this); connect(queue_, SIGNAL(rowsAboutToBeRemoved(QModelIndex, int, int)), SLOT(TracksAboutToBeDequeued(QModelIndex, int, int))); connect(queue_, SIGNAL(rowsRemoved(QModelIndex,int,int)), SLOT(TracksDequeued())); connect(queue_, SIGNAL(rowsInserted(const QModelIndex&, int, int)), SLOT(TracksEnqueued(const QModelIndex&, int, int))); connect(queue_, SIGNAL(layoutChanged()), SLOT(QueueLayoutChanged())); column_alignments_ = PlaylistView::DefaultColumnAlignment(); } Playlist::~Playlist() { items_.clear(); collection_items_by_id_.clear(); } template void Playlist::InsertSongItems(const SongList &songs, int pos, bool play_now, bool enqueue) { PlaylistItemList items; for (const Song &song : songs) { items << PlaylistItemPtr(new T(song)); } InsertItems(items, pos, play_now, enqueue); } QVariant Playlist::headerData(int section, Qt::Orientation, int role) const { if (role != Qt::DisplayRole && role != Qt::ToolTipRole) return QVariant(); const QString name = column_name((Playlist::Column)section); if (!name.isEmpty()) return name; return QVariant(); } bool Playlist::column_is_editable(Playlist::Column column) { switch (column) { case Column_Title: case Column_Artist: case Column_Album: case Column_AlbumArtist: case Column_Composer: case Column_Performer: case Column_Grouping: case Column_Track: case Column_Disc: case Column_Year: case Column_Genre: case Column_Comment: return true; default: break; } return false; } bool Playlist::set_column_value(Song &song, Playlist::Column column, const QVariant &value) { if (!song.IsEditable()) return false; switch (column) { case Column_Title: song.set_title(value.toString()); break; case Column_Artist: song.set_artist(value.toString()); break; case Column_Album: song.set_album(value.toString()); break; case Column_AlbumArtist: song.set_albumartist(value.toString()); break; case Column_Composer: song.set_composer(value.toString()); break; case Column_Performer: song.set_performer(value.toString()); break; case Column_Grouping: song.set_grouping(value.toString()); break; case Column_Track: song.set_track(value.toInt()); break; case Column_Disc: song.set_disc(value.toInt()); break; case Column_Year: song.set_year(value.toInt()); break; case Column_Genre: song.set_genre(value.toString()); break; case Column_Comment: song.set_comment(value.toString()); break; default: break; } return true; } QVariant Playlist::data(const QModelIndex &index, int role) const { switch (role) { case Role_IsCurrent: return current_item_index_.isValid() && index.row() == current_item_index_.row(); case Role_IsPaused: return current_is_paused_; case Role_StopAfter: return stop_after_.isValid() && stop_after_.row() == index.row(); case Role_QueuePosition: return queue_->PositionOf(index); case Qt::EditRole: case Qt::ToolTipRole: case Qt::DisplayRole: { PlaylistItemPtr item = items_[index.row()]; Song song = item->Metadata(); // Don't forget to change Playlist::CompareItems when adding new columns switch (index.column()) { case Column_Title: return song.PrettyTitle(); case Column_Artist: return song.artist(); case Column_Album: return song.album(); case Column_Length: return song.length_nanosec(); case Column_Track: return song.track(); case Column_Disc: return song.disc(); case Column_Year: return song.year(); case Column_OriginalYear: return song.effective_originalyear(); case Column_Genre: return song.genre(); case Column_AlbumArtist: return song.playlist_albumartist(); case Column_Composer: return song.composer(); case Column_Performer: return song.performer(); case Column_Grouping: return song.grouping(); case Column_PlayCount: return song.playcount(); case Column_SkipCount: return song.skipcount(); case Column_LastPlayed: return song.lastplayed(); case Column_Samplerate: return song.samplerate(); case Column_Bitdepth: return song.bitdepth(); case Column_Bitrate: return song.bitrate(); case Column_SamplerateBitdepth: return song.SampleRateBitDepthToText(); case Column_Filename: return song.url(); case Column_BaseFilename: return song.basefilename(); case Column_Filesize: return song.filesize(); case Column_Filetype: return song.filetype(); case Column_DateModified: return song.mtime(); case Column_DateCreated: return song.ctime(); case Column_Comment: if (role == Qt::DisplayRole) return song.comment().simplified(); return song.comment(); //case Column_Source: return item->Url(); } return QVariant(); } case Qt::TextAlignmentRole: return QVariant(column_alignments_.value(index.column(), (Qt::AlignLeft | Qt::AlignVCenter))); case Qt::ForegroundRole: if (data(index, Role_IsCurrent).toBool()) { // Ignore any custom colours for the currently playing item - they might clash with the glowing current track indicator. return QVariant(); } if (items_[index.row()]->HasCurrentForegroundColor()) { return QBrush(items_[index.row()]->GetCurrentForegroundColor()); } //if (index.row() < dynamic_history_length()) { //return QBrush(kDynamicHistoryColor); //} return QVariant(); case Qt::BackgroundRole: if (data(index, Role_IsCurrent).toBool()) { // Ignore any custom colours for the currently playing item - they might clash with the glowing current track indicator. return QVariant(); } if (items_[index.row()]->HasCurrentBackgroundColor()) { return QBrush(items_[index.row()]->GetCurrentBackgroundColor()); } return QVariant(); case Qt::FontRole: if (items_[index.row()]->GetShouldSkip()) { QFont track_font; track_font.setStrikeOut(true); return track_font; } return QVariant(); default: return QVariant(); } } bool Playlist::setData(const QModelIndex &index, const QVariant &value, int role) { int row = index.row(); PlaylistItemPtr item = item_at(row); Song song = item->Metadata(); if (index.data() == value) return false; if (!set_column_value(song, (Column)index.column(), value)) return false; TagReaderReply *reply = TagReaderClient::Instance()->SaveFile( song.url().toLocalFile(), song); NewClosure(reply, SIGNAL(Finished(bool)), this, SLOT(SongSaveComplete(TagReaderReply*, QPersistentModelIndex)), reply, QPersistentModelIndex(index)); return true; } void Playlist::SongSaveComplete(TagReaderReply *reply, const QPersistentModelIndex &index) { if (reply->is_successful() && index.isValid()) { if (reply->message().save_file_response().success()) { QFuture future = item_at(index.row())->BackgroundReload(); NewClosure(future, this, SLOT(ItemReloadComplete(QPersistentModelIndex)), index); } else { emit Error(tr("An error occurred writing metadata to '%1'").arg(QString::fromStdString(reply->request_message().save_file_request().filename()))); } } reply->deleteLater(); } void Playlist::ItemReloadComplete(const QPersistentModelIndex &index) { if (index.isValid()) { emit dataChanged(index, index); emit EditingFinished(index); } } int Playlist::current_row() const { return current_item_index_.isValid() ? current_item_index_.row() : -1; } const QModelIndex Playlist::current_index() const { return current_item_index_; } int Playlist::last_played_row() const { return last_played_item_index_.isValid() ? last_played_item_index_.row() : -1; } void Playlist::ShuffleModeChanged(PlaylistSequence::ShuffleMode mode) { is_shuffled_ = (mode != PlaylistSequence::Shuffle_Off); ReshuffleIndices(); } bool Playlist::FilterContainsVirtualIndex(int i) const { if (i < 0 || i >= virtual_items_.count()) return false; return proxy_->filterAcceptsRow(virtual_items_[i], QModelIndex()); } int Playlist::NextVirtualIndex(int i, bool ignore_repeat_track) const { PlaylistSequence::RepeatMode repeat_mode = playlist_sequence_->repeat_mode(); PlaylistSequence::ShuffleMode shuffle_mode = playlist_sequence_->shuffle_mode(); bool album_only = repeat_mode == PlaylistSequence::Repeat_Album || shuffle_mode == PlaylistSequence::Shuffle_InsideAlbum; // This one's easy - if we have to repeat the current track then just return i if (repeat_mode == PlaylistSequence::Repeat_Track && !ignore_repeat_track) { if (!FilterContainsVirtualIndex(i)) return virtual_items_.count(); // It's not in the filter any more return i; } // If we're not bothered about whether a song is on the same album then return the next virtual index, whatever it is. if (!album_only) { ++i; // Advance i until we find any track that is in the filter, skipping the selected to be skipped while (i < virtual_items_.count() && (!FilterContainsVirtualIndex(i) || item_at(virtual_items_[i])->GetShouldSkip())) { ++i; } return i; } // We need to advance i until we get something else on the same album Song last_song = current_item_metadata(); for (int j = i + 1; j < virtual_items_.count(); ++j) { if (item_at(virtual_items_[j])->GetShouldSkip()) { continue; } Song this_song = item_at(virtual_items_[j])->Metadata(); if (((last_song.is_compilation() && this_song.is_compilation()) || last_song.artist() == this_song.artist()) && last_song.album() == this_song.album() && FilterContainsVirtualIndex(j)) { return j; // Found one } } // Couldn't find one - return past the end of the list return virtual_items_.count(); } int Playlist::PreviousVirtualIndex(int i, bool ignore_repeat_track) const { PlaylistSequence::RepeatMode repeat_mode = playlist_sequence_->repeat_mode(); PlaylistSequence::ShuffleMode shuffle_mode = playlist_sequence_->shuffle_mode(); bool album_only = repeat_mode == PlaylistSequence::Repeat_Album || shuffle_mode == PlaylistSequence::Shuffle_InsideAlbum; // This one's easy - if we have to repeat the current track then just return i if (repeat_mode == PlaylistSequence::Repeat_Track && !ignore_repeat_track) { if (!FilterContainsVirtualIndex(i)) return -1; return i; } // If we're not bothered about whether a song is on the same album then return the previous virtual index, whatever it is. if (!album_only) { --i; // Decrement i until we find any track that is in the filter while (i >= 0 && (!FilterContainsVirtualIndex(i) || item_at(virtual_items_[i])->GetShouldSkip())) --i; return i; } // We need to decrement i until we get something else on the same album Song last_song = current_item_metadata(); for (int j = i - 1; j >= 0; --j) { if (item_at(virtual_items_[j])->GetShouldSkip()) { continue; } Song this_song = item_at(virtual_items_[j])->Metadata(); if (((last_song.is_compilation() && this_song.is_compilation()) || last_song.artist() == this_song.artist()) && last_song.album() == this_song.album() && FilterContainsVirtualIndex(j)) { return j; // Found one } } // Couldn't find one - return before the start of the list return -1; } int Playlist::next_row(bool ignore_repeat_track) const { // Any queued items take priority if (!queue_->is_empty()) { return queue_->PeekNext(); } int next_virtual_index = NextVirtualIndex(current_virtual_index_, ignore_repeat_track); if (next_virtual_index >= virtual_items_.count()) { // We've gone off the end of the playlist. switch (playlist_sequence_->repeat_mode()) { case PlaylistSequence::Repeat_Off: case PlaylistSequence::Repeat_Intro: return -1; case PlaylistSequence::Repeat_Track: next_virtual_index = current_virtual_index_; break; default: next_virtual_index = NextVirtualIndex(-1, ignore_repeat_track); break; } } // Still off the end? Then just give up if (next_virtual_index < 0 || next_virtual_index >= virtual_items_.count()) return -1; return virtual_items_[next_virtual_index]; } int Playlist::previous_row(bool ignore_repeat_track) const { int prev_virtual_index = PreviousVirtualIndex(current_virtual_index_,ignore_repeat_track); if (prev_virtual_index < 0) { // We've gone off the beginning of the playlist. switch (playlist_sequence_->repeat_mode()) { case PlaylistSequence::Repeat_Off: return -1; case PlaylistSequence::Repeat_Track: prev_virtual_index = current_virtual_index_; break; default: prev_virtual_index = PreviousVirtualIndex(virtual_items_.count(),ignore_repeat_track); break; } } // Still off the beginning? Then just give up if (prev_virtual_index < 0) return -1; return virtual_items_[prev_virtual_index]; } void Playlist::set_current_row(int i, bool is_stopping) { QModelIndex old_current_item_index = current_item_index_; //ClearStreamMetadata(); current_item_index_ = QPersistentModelIndex(index(i, 0, QModelIndex())); // if the given item is the first in the queue, remove it from the queue if (current_item_index_.row() == queue_->PeekNext()) { queue_->TakeNext(); } if (current_item_index_ == old_current_item_index) return; if (old_current_item_index.isValid()) { emit dataChanged(old_current_item_index, old_current_item_index.sibling(old_current_item_index.row(), ColumnCount - 1)); } // Update the virtual index if (i == -1) { current_virtual_index_ = -1; } else if (is_shuffled_ && current_virtual_index_ == -1) { // This is the first thing we're playing so we want to make sure the array is shuffled ReshuffleIndices(); // Bring the one we've been asked to play to the start of the list virtual_items_.takeAt(virtual_items_.indexOf(i)); virtual_items_.prepend(i); current_virtual_index_ = 0; } else if (is_shuffled_) { current_virtual_index_ = virtual_items_.indexOf(i); } else { current_virtual_index_ = i; } if (current_item_index_.isValid() && !is_stopping) { InformOfCurrentSongChange(); } if (current_item_index_.isValid()) { last_played_item_index_ = current_item_index_; Save(); } } Qt::ItemFlags Playlist::flags(const QModelIndex &index) const { Qt::ItemFlags flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable; if (column_is_editable((Column)index.column())) flags |= Qt::ItemIsEditable; if (index.isValid()) return flags | Qt::ItemIsDragEnabled; return Qt::ItemIsDropEnabled; } QStringList Playlist::mimeTypes() const { return QStringList() << "text/uri-list" << kRowsMimetype; } Qt::DropActions Playlist::supportedDropActions() const { return Qt::MoveAction | Qt::CopyAction | Qt::LinkAction; } bool Playlist::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int, const QModelIndex&) { if (action == Qt::IgnoreAction) return false; bool play_now = false; bool enqueue_now = false; if (const MimeData *mime_data = qobject_cast(data)) { if (mime_data->clear_first_) { Clear(); } play_now = mime_data->play_now_; enqueue_now = mime_data->enqueue_now_; } if (const SongMimeData *song_data = qobject_cast(data)) { // Dragged from a collection // We want to check if these songs are from the actual local file backend, if they are we treat them differently. if (song_data->backend && song_data->backend->songs_table() == SCollection::kSongsTable) InsertSongItems(song_data->songs, row, play_now, enqueue_now); else InsertSongItems(song_data->songs, row, play_now, enqueue_now); } else if (const PlaylistItemMimeData *item_data = qobject_cast(data)) { InsertItems(item_data->items_, row, play_now, enqueue_now); } else if (data->hasFormat(kRowsMimetype)) { // Dragged from the playlist // Rearranging it is tricky... // Get the list of rows that were moved QList source_rows; Playlist *source_playlist = nullptr; qint64 pid = 0; qint64 own_pid = QCoreApplication::applicationPid(); QDataStream stream(data->data(kRowsMimetype)); stream.readRawData(reinterpret_cast(&source_playlist), sizeof(source_playlist)); stream >> source_rows; if (!stream.atEnd()) { stream.readRawData((char*)&pid, sizeof(pid)); } else { pid = !own_pid; } qStableSort(source_rows); // Make sure we take them in order if (source_playlist == this) { // Dragged from this playlist - rearrange the items undo_stack_->push(new PlaylistUndoCommands::MoveItems(this, source_rows, row)); } else if (pid == own_pid) { // Drag from a different playlist PlaylistItemList items; for (int row : source_rows) items << source_playlist->item_at(row); if (items.count() > kUndoItemLimit) { // Too big to keep in the undo stack. Also clear the stack because it might have been invalidated. InsertItemsWithoutUndo(items, row, false); undo_stack_->clear(); } else { undo_stack_->push(new PlaylistUndoCommands::InsertItems(this, items, row)); } // Remove the items from the source playlist if it was a move event if (action == Qt::MoveAction) { for (int row : source_rows) { source_playlist->undo_stack()->push(new PlaylistUndoCommands::RemoveItems(source_playlist, row, 1)); } } } } else if (data->hasFormat(kCddaMimeType)) { SongLoaderInserter *inserter = new SongLoaderInserter(task_manager_, collection_, backend_->app()->player()); connect(inserter, SIGNAL(Error(QString)), SIGNAL(Error(QString))); inserter->LoadAudioCD(this, row, play_now, enqueue_now); } else if (data->hasUrls()) { // URL list dragged from the file list or some other app InsertUrls(data->urls(), row, play_now, enqueue_now); } return true; } void Playlist::InsertUrls(const QList &urls, int pos, bool play_now, bool enqueue) { SongLoaderInserter *inserter = new SongLoaderInserter(task_manager_, collection_, backend_->app()->player()); connect(inserter, SIGNAL(Error(QString)), SIGNAL(Error(QString))); inserter->Load(this, pos, play_now, enqueue, urls); } void Playlist::MoveItemWithoutUndo(int source, int dest) { MoveItemsWithoutUndo(QList() << source, dest); } void Playlist::MoveItemsWithoutUndo(const QList &source_rows, int pos) { layoutAboutToBeChanged(); PlaylistItemList moved_items; if (pos < 0) { pos = items_.count(); } // Take the items out of the list first, keeping track of whether the insertion point changes int offset = 0; int start = pos; for (int source_row : source_rows) { moved_items << items_.takeAt(source_row - offset); if (pos > source_row) { start--; } offset++; } // Put the items back in for (int i = start; i < start + moved_items.count(); ++i) { moved_items[i - start]->RemoveForegroundColor(kDynamicHistoryPriority); items_.insert(i, moved_items[i - start]); } // Update persistent indexes for (const QModelIndex &pidx : persistentIndexList()) { const int dest_offset = source_rows.indexOf(pidx.row()); if (dest_offset != -1) { // This index was moved changePersistentIndex(pidx, index(start + dest_offset, pidx.column(), QModelIndex())); } else { int d = 0; for (int source_row : source_rows) { if (pidx.row() > source_row) d--; } if (pidx.row() + d >= start) d += source_rows.count(); changePersistentIndex(pidx, index(pidx.row() + d, pidx.column(), QModelIndex())); } } current_virtual_index_ = virtual_items_.indexOf(current_row()); layoutChanged(); Save(); } void Playlist::MoveItemsWithoutUndo(int start, const QList &dest_rows) { layoutAboutToBeChanged(); PlaylistItemList moved_items; int pos = start; for (int dest_row : dest_rows) { if (dest_row < pos) start--; } if (start < 0) { start = items_.count() - dest_rows.count(); } // Take the items out of the list first for (int i = 0; i < dest_rows.count(); i++) moved_items << items_.takeAt(start); // Put the items back in int offset = 0; for (int dest_row : dest_rows) { items_.insert(dest_row, moved_items[offset]); offset++; } // Update persistent indexes for (const QModelIndex &pidx : persistentIndexList()) { if (pidx.row() >= start && pidx.row() < start + dest_rows.count()) { // This index was moved const int i = pidx.row() - start; changePersistentIndex(pidx, index(dest_rows[i], pidx.column(), QModelIndex())); } else { int d = 0; if (pidx.row() >= start + dest_rows.count()) d -= dest_rows.count(); for (int dest_row : dest_rows) { if (pidx.row() + d > dest_row) d++; } changePersistentIndex(pidx, index(pidx.row() + d, pidx.column(), QModelIndex())); } } current_virtual_index_ = virtual_items_.indexOf(current_row()); layoutChanged(); Save(); } void Playlist::InsertItems(const PlaylistItemList &itemsIn, int pos, bool play_now, bool enqueue) { if (itemsIn.isEmpty()) return; PlaylistItemList items = itemsIn; // exercise vetoes SongList songs; for (PlaylistItemPtr item : items) { songs << item->Metadata(); } const int song_count = songs.length(); QSet vetoed; for (SongInsertVetoListener *listener : veto_listeners_) { for (const Song &song : listener->AboutToInsertSongs(GetAllSongs(), songs)) { // avoid veto-ing a song multiple times vetoed.insert(song); } if (vetoed.count() == song_count) { // all songs were vetoed and there's nothing more to do (there's no need for an undo step) return; } } if (!vetoed.isEmpty()) { QMutableListIterator it(items); while (it.hasNext()) { PlaylistItemPtr item = it.next(); const Song ¤t = item->Metadata(); if (vetoed.contains(current)) { vetoed.remove(current); it.remove(); } } // check for empty items once again after veto if (items.isEmpty()) { return; } } const int start = pos == -1 ? items_.count() : pos; if (items.count() > kUndoItemLimit) { // Too big to keep in the undo stack. Also clear the stack because it might have been invalidated. InsertItemsWithoutUndo(items, pos, enqueue); undo_stack_->clear(); } else { undo_stack_->push(new PlaylistUndoCommands::InsertItems(this, items, pos, enqueue)); } if (play_now) emit PlayRequested(index(start, 0)); } void Playlist::InsertItemsWithoutUndo(const PlaylistItemList &items, int pos, bool enqueue) { if (items.isEmpty()) return; const int start = pos == -1 ? items_.count() : pos; const int end = start + items.count() - 1; beginInsertRows(QModelIndex(), start, end); for (int i = start; i <= end; ++i) { PlaylistItemPtr item = items[i - start]; items_.insert(i, item); virtual_items_ << virtual_items_.count(); if (item->type() == "Collection") { int id = item->Metadata().id(); if (id != -1) { collection_items_by_id_.insertMulti(id, item); } } if (item == current_item()) { // It's one we removed before that got re-added through an undo current_item_index_ = index(i, 0); last_played_item_index_ = current_item_index_; } } endInsertRows(); if (enqueue) { QModelIndexList indexes; for (int i = start; i <= end; ++i) { indexes << index(i, 0); } queue_->ToggleTracks(indexes); } Save(); ReshuffleIndices(); } void Playlist::InsertCollectionItems(const SongList &songs, int pos, bool play_now, bool enqueue) { InsertSongItems(songs, pos, play_now, enqueue); } void Playlist::InsertSongs(const SongList &songs, int pos, bool play_now, bool enqueue) { InsertSongItems(songs, pos, play_now, enqueue); } void Playlist::InsertSongsOrCollectionItems(const SongList &songs, int pos, bool play_now, bool enqueue) { PlaylistItemList items; for (const Song &song : songs) { if (song.is_collection_song()) { items << PlaylistItemPtr(new CollectionPlaylistItem(song)); } else { items << PlaylistItemPtr(new SongPlaylistItem(song)); } } InsertItems(items, pos, play_now, enqueue); } void Playlist::UpdateItems(const SongList &songs) { qLog(Debug) << "Updating playlist with new tracks' info"; // We first convert our songs list into a linked list (a 'real' list), because removals are faster with QLinkedList. // Next, we walk through the list of playlist's items then the list of songs // we want to update: if an item corresponds to the song (we rely on URL for this), we update the item with the new metadata, // then we remove song from our list because we will not need to check it again. // And we also update undo actions. QLinkedList songs_list; for (const Song &song : songs) songs_list.append(song); for (int i = 0; i < items_.size(); i++) { // Update current items list QMutableLinkedListIterator it(songs_list); while (it.hasNext()) { const Song &song = it.next(); PlaylistItemPtr &item = items_[i]; if (item->Metadata().url() == song.url() && (item->Metadata().filetype() == Song::Type_Unknown || // And CD tracks as well (tags are loaded in a second step) item->Metadata().filetype() == Song::Type_Cdda)) { PlaylistItemPtr new_item; if (song.is_collection_song()) { new_item = PlaylistItemPtr(new CollectionPlaylistItem(song)); collection_items_by_id_.insertMulti(song.id(), new_item); } else { new_item = PlaylistItemPtr(new SongPlaylistItem(song)); } items_[i] = new_item; emit dataChanged(index(i, 0), index(i, ColumnCount - 1)); // Also update undo actions for (int i = 0; i < undo_stack_->count(); i++) { QUndoCommand *undo_action = const_cast(undo_stack_->command(i)); PlaylistUndoCommands::InsertItems *undo_action_insert = dynamic_cast(undo_action); if (undo_action_insert) { bool found_and_updated = undo_action_insert->UpdateItem(new_item); if (found_and_updated) break; } } it.remove(); break; } } } Save(); } QMimeData *Playlist::mimeData(const QModelIndexList &indexes) const { if (indexes.isEmpty()) return nullptr; // We only want one index per row, but we can't just take column 0 because the user might have hidden it. const int first_column = indexes.first().column(); QMimeData *data = new QMimeData; QList urls; QList rows; for (const QModelIndex &index : indexes) { if (index.column() != first_column) continue; urls << items_[index.row()]->Url(); rows << index.row(); } QBuffer buf; buf.open(QIODevice::WriteOnly); QDataStream stream(&buf); const Playlist *self = this; const qint64 pid = QCoreApplication::applicationPid(); stream.writeRawData(reinterpret_cast(&self), sizeof(self)); stream << rows; stream.writeRawData((char*)&pid, sizeof(pid)); buf.close(); data->setUrls(urls); data->setData(kRowsMimetype, buf.data()); return data; } bool Playlist::CompareItems(int column, Qt::SortOrder order, shared_ptr _a, shared_ptr _b) { shared_ptr a = order == Qt::AscendingOrder ? _a : _b; shared_ptr b = order == Qt::AscendingOrder ? _b : _a; #define cmp(field) return a->Metadata().field() < b->Metadata().field() #define strcmp(field) return QString::localeAwareCompare(a->Metadata().field().toLower(), b->Metadata().field().toLower()) < 0; switch (column) { case Column_Title: strcmp(title); case Column_Artist: strcmp(artist); case Column_Album: strcmp(album); case Column_Length: cmp(length_nanosec); case Column_Track: cmp(track); case Column_Disc: cmp(disc); case Column_Year: cmp(year); case Column_OriginalYear: cmp(originalyear); case Column_Genre: strcmp(genre); case Column_AlbumArtist: strcmp(playlist_albumartist); case Column_Composer: strcmp(composer); case Column_Performer: strcmp(performer); case Column_Grouping: strcmp(grouping); case Column_PlayCount: cmp(playcount); case Column_SkipCount: cmp(skipcount); case Column_LastPlayed: cmp(lastplayed); case Column_Bitrate: cmp(bitrate); case Column_Samplerate: cmp(samplerate); case Column_Bitdepth: cmp(bitdepth); case Column_SamplerateBitdepth: return QString::localeAwareCompare(a->Metadata().SampleRateBitDepthToText().toLower(), b->Metadata().SampleRateBitDepthToText().toLower()) < 0; case Column_Filename: return (QString::localeAwareCompare(a->Url().path().toLower(), b->Url().path().toLower()) < 0); case Column_BaseFilename: cmp(basefilename); case Column_Filesize: cmp(filesize); case Column_Filetype: cmp(filetype); case Column_DateModified: cmp(mtime); case Column_DateCreated: cmp(ctime); case Column_Comment: strcmp(comment); //case Column_Source: cmp(url); } #undef cmp #undef strcmp return false; } bool Playlist::ComparePathDepths(Qt::SortOrder order, shared_ptr _a, shared_ptr _b) { shared_ptr a = order == Qt::AscendingOrder ? _a : _b; shared_ptr b = order == Qt::AscendingOrder ? _b : _a; int a_dir_level = a->Url().path().count('/'); int b_dir_level = b->Url().path().count('/'); return a_dir_level < b_dir_level; } QString Playlist::column_name(Column column) { switch (column) { case Column_Title: return tr("Title"); case Column_Artist: return tr("Artist"); case Column_Album: return tr("Album"); case Column_Track: return tr("Track"); case Column_Disc: return tr("Disc"); case Column_Length: return tr("Length"); case Column_Year: return tr("Year"); case Column_OriginalYear: return tr("Original year"); case Column_Genre: return tr("Genre"); case Column_AlbumArtist: return tr("Album artist"); case Column_Composer: return tr("Composer"); case Column_Performer: return tr("Performer"); case Column_Grouping: return tr("Grouping"); case Column_PlayCount: return tr("Play count"); case Column_SkipCount: return tr("Skip count"); case Column_LastPlayed: return tr("Last played"); case Column_Samplerate: return tr("Sample rate"); case Column_Bitdepth: return tr("Bit depth"); case Column_SamplerateBitdepth: return tr("Sample rate B"); case Column_Bitrate: return tr("Bitrate"); case Column_Filename: return tr("File name"); case Column_BaseFilename: return tr("File name (without path)"); case Column_Filesize: return tr("File size"); case Column_Filetype: return tr("File type"); case Column_DateModified: return tr("Date modified"); case Column_DateCreated: return tr("Date created"); case Column_Comment: return tr("Comment"); //case Column_Source: return tr("Source"); default: return QString(); } return ""; } QString Playlist::abbreviated_column_name(Column column) { const QString &column_name = Playlist::column_name(column); switch (column) { case Column_Disc: case Column_PlayCount: case Column_SkipCount: case Column_Track: return QString("%1#").arg(column_name[0]); default: return column_name; } return ""; } void Playlist::sort(int column, Qt::SortOrder order) { if (ignore_sorting_) return; PlaylistItemList new_items(items_); PlaylistItemList::iterator begin = new_items.begin(); if (column == Column_Album) { // When sorting by album, also take into account discs and tracks. qStableSort(begin, new_items.end(), std::bind(&Playlist::CompareItems, Column_Track, order, _1, _2)); qStableSort(begin, new_items.end(), std::bind(&Playlist::CompareItems, Column_Disc, order, _1, _2)); qStableSort(begin, new_items.end(), std::bind(&Playlist::CompareItems, Column_Album, order, _1, _2)); } else if (column == Column_Filename) { // When sorting by full paths we also expect a hierarchical order. This returns a breath-first ordering of paths. qStableSort(begin, new_items.end(), std::bind(&Playlist::CompareItems, Column_Filename, order, _1, _2)); qStableSort(begin, new_items.end(), std::bind(&Playlist::ComparePathDepths, order, _1, _2)); } else { qStableSort(begin, new_items.end(), std::bind(&Playlist::CompareItems, column, order, _1, _2)); } undo_stack_->push(new PlaylistUndoCommands::SortItems(this, column, order, new_items)); ReshuffleIndices(); } void Playlist::ReOrderWithoutUndo(const PlaylistItemList &new_items) { layoutAboutToBeChanged(); PlaylistItemList old_items = items_; items_ = new_items; QMap new_rows; for (int i = 0; i < new_items.length(); ++i) { new_rows[new_items[i].get()] = i; } for (const QModelIndex &idx : persistentIndexList()) { const PlaylistItem *item = old_items[idx.row()].get(); changePersistentIndex(idx, index(new_rows[item], idx.column(), idx.parent())); } layoutChanged(); emit PlaylistChanged(); Save(); } void Playlist::Playing() { SetCurrentIsPaused(false); } void Playlist::Paused() { SetCurrentIsPaused(true); } void Playlist::Stopped() { SetCurrentIsPaused(false); } void Playlist::SetCurrentIsPaused(bool paused) { if (paused == current_is_paused_) return; current_is_paused_ = paused; if (current_item_index_.isValid()) dataChanged(index(current_item_index_.row(), 0), index(current_item_index_.row(), ColumnCount - 1)); } void Playlist::Save() const { if (!backend_ || is_loading_) return; backend_->SavePlaylistAsync(id_, items_, last_played_row()); } //namespace { //typedef QFutureWatcher> PlaylistItemFutureWatcher; //} void Playlist::Restore() { if (!backend_) return; items_.clear(); virtual_items_.clear(); collection_items_by_id_.clear(); cancel_restore_ = false; QFuture> future = QtConcurrent::run(backend_, &PlaylistBackend::GetPlaylistItems, id_); NewClosure(future, this, SLOT(ItemsLoaded(QFuture)), future); } void Playlist::ItemsLoaded(QFuture future) { if (cancel_restore_) return; PlaylistItemList items = future.result(); // Backend returns empty elements for collection items which it couldn't match (because they got deleted); we don't need those QMutableListIterator it(items); while (it.hasNext()) { PlaylistItemPtr item = it.next(); if (item->IsLocalCollectionItem() && item->Metadata().url().isEmpty()) { it.remove(); } } is_loading_ = true; InsertItems(items, 0); is_loading_ = false; PlaylistBackend::Playlist p = backend_->GetPlaylist(id_); // The newly loaded list of items might be shorter than it was before so look out for a bad last_played index last_played_item_index_ = p.last_played == -1 || p.last_played >= rowCount() ? QModelIndex() : index(p.last_played); emit RestoreFinished(); QSettings s; s.beginGroup(kSettingsGroup); // Should we gray out deleted songs asynchronously on startup? if (s.value("greyoutdeleted", false).toBool()) { QtConcurrent::run(this, &Playlist::InvalidateDeletedSongs); } } static bool DescendingIntLessThan(int a, int b) { return a > b; } void Playlist::RemoveItemsWithoutUndo(const QList &indicesIn) { // Sort the indices descending because removing elements 'backwards' is easier - indices don't 'move' in the process. QList indices = indicesIn; qSort(indices.begin(), indices.end(), DescendingIntLessThan); for (int j = 0; j < indices.count(); j++) { int beginning = indices[j], end = indices[j]; // Splits the indices into sequences. For example this: [1, 2, 4], will get split into [1, 2] and [4]. while (j != indices.count() - 1 && indices[j] == indices[j + 1] + 1) { beginning--; j++; } // Remove the current sequence. removeRows(beginning, end - beginning + 1); } } bool Playlist::removeRows(int row, int count, const QModelIndex &parent) { if (row < 0 || row >= items_.size() || row + count > items_.size()) { return false; } if (count > kUndoItemLimit) { // Too big to keep in the undo stack. Also clear the stack because it might have been invalidated. RemoveItemsWithoutUndo(row, count); undo_stack_->clear(); } else if (parent == QModelIndex()) { RemoveItemsWithoutUndo(row, count); } else { undo_stack_->push(new PlaylistUndoCommands::RemoveItems(this, row, count)); } return true; } bool Playlist::removeRows(QList &rows) { if (rows.isEmpty()) { return false; } // Start from the end to be sure that indices won't 'move' during the removal process qSort(rows.begin(), rows.end(), qGreater()); QList part; while (!rows.isEmpty()) { // we're splitting the input list into sequences of consecutive numbers part.append(rows.takeFirst()); while (!rows.isEmpty() && rows.first() == part.last() - 1) { part.append(rows.takeFirst()); } // and now we're removing the current sequence if (!removeRows(part.last(), part.size())) { return false; } part.clear(); } return true; } PlaylistItemList Playlist::RemoveItemsWithoutUndo(int row, int count) { if (row < 0 || row >= items_.size() || row + count > items_.size()) { return PlaylistItemList(); } beginRemoveRows(QModelIndex(), row, row + count - 1); // Remove items PlaylistItemList ret; for (int i = 0; i < count; ++i) { PlaylistItemPtr item(items_.takeAt(row)); ret << item; if (item->type() == "Collection") { int id = item->Metadata().id(); if (id != -1) { collection_items_by_id_.remove(id, item); } } } endRemoveRows(); QList::iterator it = virtual_items_.begin(); int i = 0; while (it != virtual_items_.end()) { if (*it >= items_.count()) it = virtual_items_.erase(it); else ++it; ++i; } // Reset current_virtual_index_ if (current_row() == -1) if (row - 1 > 0 && row - 1 < items_.size()) { current_virtual_index_ = virtual_items_.indexOf(row - 1); } else { current_virtual_index_ = -1; } else current_virtual_index_ = virtual_items_.indexOf(current_row()); Save(); return ret; } void Playlist::StopAfter(int row) { QModelIndex old_stop_after = stop_after_; if ((stop_after_.isValid() && stop_after_.row() == row) || row == -1) stop_after_ = QModelIndex(); else stop_after_ = index(row, 0); if (old_stop_after.isValid()) emit dataChanged(old_stop_after, old_stop_after.sibling(old_stop_after.row(), ColumnCount - 1)); if (stop_after_.isValid()) emit dataChanged(stop_after_, 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); InformOfCurrentSongChange(); } void Playlist::ClearStreamMetadata() { if (!current_item()) return; current_item()->ClearTemporaryMetadata(); emit dataChanged(index(current_item_index_.row(), 0), index(current_item_index_.row(), ColumnCount-1)); } bool Playlist::stop_after_current() const { PlaylistSequence::RepeatMode repeat_mode = playlist_sequence_->repeat_mode(); if (repeat_mode == PlaylistSequence::Repeat_OneByOne) { return true; } 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::Clear() { // If loading songs from session restore async, don't insert them cancel_restore_ = true; 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)); } Save(); } void Playlist::RemoveItemsNotInQueue() { if (queue_->is_empty() && !current_item_index_.isValid()) { RemoveItemsWithoutUndo(0, items_.count()); return; } int start = 0; forever { // Find a place to start - first row that isn't in the queue forever { if (start >= rowCount()) return; if (!queue_->ContainsSourceRow(start) && current_row() != 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) || current_row() == start + count) break; count++; } RemoveItemsWithoutUndo(start, count); start++; } } void Playlist::ReloadItems(const QList &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)); } } Save(); } 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(sender())); } void Playlist::Shuffle() { PlaylistItemList new_items(items_); int begin = 0; 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 &album_key_positions, const QMap &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::iterator begin = virtual_items_.begin(); QList::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 album_keys; // real index -> key QSet album_key_set; // unique keys // Find all the unique albums in the playlist for (QList::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 // Or if the song was not playing but it was selected, force its album to be first. if (current_virtual_index_ != -1 || current_row() != -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 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::collection_items_by_id(int id) const { return collection_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(); emit QueueChanged(); } 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 invalidated_rows; for (int row = 0; row < items_.count(); ++row) { PlaylistItemPtr item = items_[row]; Song song = item->Metadata(); 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 rows_to_remove; for (int row = 0; row < items_.count(); ++row) { PlaylistItemPtr item = items_[row]; Song song = item->Metadata(); if (!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 rows_to_remove; unordered_map 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); } void Playlist::RemoveUnavailableSongs() { QList rows_to_remove; for (int row = 0; row < items_.count(); ++row) { PlaylistItemPtr item = items_[row]; const Song &song = item->Metadata(); // Check only local files if (song.url().isLocalFile() && !QFile::exists(song.url().toLocalFile())) { rows_to_remove.append(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_cdda() && current_song.url() == url && current_song.is_valid() != QFile::exists(current_song.url().toLocalFile())) { ReloadItems(QList() << 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(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())); emit dataChanged(source_index, source_index); } }