/* This file is part of Clementine. Clementine 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. Clementine 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 Clementine. If not, see . */ #include "playlist.h" #include "songmimedata.h" #include "songplaylistitem.h" #include "radiomimedata.h" #include "radioplaylistitem.h" #include "radiomodel.h" #include "savedradio.h" #include #include #include #include #include #include #include #include #include const char* Playlist::kRowsMimetype = "application/x-clementine-playlist-rows"; const char* Playlist::kSettingsGroup = "Playlist"; Playlist::Playlist(QObject *parent) : QAbstractListModel(parent), current_is_paused_(false), current_virtual_index_(-1), is_shuffled_(false), scrobble_point_(-1), has_scrobbled_(false), playlist_sequence_(NULL), ignore_sorting_(false) { } Playlist::~Playlist() { qDeleteAll(items_); } QVariant Playlist::headerData(int section, Qt::Orientation, int role) const { if (role != Qt::DisplayRole) return QVariant(); switch (section) { case Column_Title: return tr("Title"); case Column_Artist: return tr("Artist"); case Column_Album: return tr("Album"); case Column_Length: return tr("Length"); case Column_Track: return tr("Track"); case Column_Disc: return tr("Disc"); case Column_Year: return tr("Year"); case Column_Genre: return tr("Genre"); case Column_AlbumArtist: return tr("Album artist"); case Column_Composer: return tr("Composer"); case Column_BPM: return tr("BPM"); case Column_Bitrate: return tr("Bit rate"); case Column_Samplerate: return tr("Sample rate"); 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"); } return QVariant(); } QVariant Playlist::data(const QModelIndex& index, int role) const { switch (role) { case Role_IsCurrent: return current_item_.isValid() && index.row() == current_item_.row(); case Role_IsPaused: return current_is_paused_; case Role_StopAfter: return stop_after_.isValid() && stop_after_.row() == index.row(); case Qt::DisplayRole: { PlaylistItem* 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.title(); case Column_Artist: return song.artist(); case Column_Album: return song.album(); case Column_Length: return song.length(); case Column_Track: return song.track(); case Column_Disc: return song.disc(); case Column_Year: return song.year(); case Column_Genre: return song.genre(); case Column_AlbumArtist: return song.albumartist(); case Column_Composer: return song.composer(); case Column_BPM: return song.bpm(); case Column_Bitrate: return song.bitrate(); case Column_Samplerate: return song.samplerate(); case Column_Filename: return song.filename(); 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(); } } default: return QVariant(); } } int Playlist::current_index() const { return current_item_.isValid() ? current_item_.row() : -1; } void Playlist::ShuffleModeChanged(PlaylistSequence::ShuffleMode mode) { is_shuffled_ = (mode != PlaylistSequence::Shuffle_Off); ReshuffleIndices(); } int Playlist::NextVirtualIndex(int i) 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_Album; // This one's easy - if we have to repeat the current track then just return i if (repeat_mode == PlaylistSequence::Repeat_Track) 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) return i+1; // 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 ; jMetadata(); if ((last_song.is_compilation() && this_song.is_compilation() || last_song.artist() == this_song.artist()) && last_song.album() == this_song.album()) { return j; // Found one } } // Couldn't find one - return past the end of the list return virtual_items_.count(); } int Playlist::next_index() const { // Did we want to stop after this track? if (stop_after_.isValid() && current_index() == stop_after_.row()) return -1; int next_virtual_index = NextVirtualIndex(current_virtual_index_); 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: return -1; case PlaylistSequence::Repeat_Track: next_virtual_index = current_virtual_index_; break; default: next_virtual_index = NextVirtualIndex(-1); break; } } // Still off the end? Then just give up if (next_virtual_index >= virtual_items_.count()) return -1; return virtual_items_[next_virtual_index]; } int Playlist::previous_index() const { int i = current_index() - 1; if (i < 0) return -1; return i; } void Playlist::set_current_index(int i) { QModelIndex old_current = current_item_; ClearStreamMetadata(); current_item_ = QPersistentModelIndex(index(i, 0, QModelIndex())); if (old_current.isValid()) emit dataChanged(old_current, old_current.sibling(old_current.row(), ColumnCount)); if (current_item_.isValid() && current_item_ != old_current) { emit dataChanged(current_item_, current_item_.sibling(current_item_.row(), ColumnCount)); emit CurrentSongChanged(current_item_metadata()); } // 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; UpdateScrobblePoint(); } Qt::ItemFlags Playlist::flags(const QModelIndex &index) const { if (index.isValid()) return Qt::ItemIsDragEnabled | Qt::ItemIsEnabled | Qt::ItemIsSelectable; return Qt::ItemIsDropEnabled | Qt::ItemIsEnabled | Qt::ItemIsSelectable; } QStringList Playlist::mimeTypes() const { return QStringList() << "text/uri-list" << kRowsMimetype; } Qt::DropActions Playlist::supportedDropActions() const { return Qt::MoveAction | Qt::CopyAction; } bool Playlist::dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int, const QModelIndex&) { if (action == Qt::IgnoreAction) return false; if (const SongMimeData* song_data = qobject_cast(data)) { // Dragged from the library InsertSongs(song_data->songs, row); } else if (const RadioMimeData* radio_data = qobject_cast(data)) { // Dragged from the Radio pane InsertRadioStations(radio_data->items, row); } else if (data->hasFormat(kRowsMimetype)) { // Dragged from the playlist // Rearranging it is tricky... // Get the list of rows that were moved QList source_rows; QDataStream stream(data->data(kRowsMimetype)); stream >> source_rows; qStableSort(source_rows); // Make sure we take them in order layoutAboutToBeChanged(); QList moved_items; // Take the items out of the list first, keeping track of whether the // insertion point changes int offset = 0; foreach (int source_row, source_rows) { moved_items << items_.takeAt(source_row-offset); if (row != -1 && row >= source_row) row --; offset++; } // Put the items back in const int start = row == -1 ? items_.count() : row; for (int i=start ; i 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_index()); layoutChanged(); Save(); } else if (data->hasUrls()) { // URL list dragged from the file list or some other app InsertPaths(data->urls(), row); } return true; } QModelIndex Playlist::InsertPaths(QList urls, int after) { SongList songs; for (int i=0 ; i new_urls; while (it.hasNext()) { QString path(it.next()); new_urls << QUrl::fromLocalFile(path); } qSort(new_urls); urls << new_urls; } else { Song song; song.InitFromFile(filename, -1); if (!song.is_valid()) continue; songs << song; } } return InsertSongs(songs, after); } QModelIndex Playlist::InsertItems(const QList& items, int after) { if (items.isEmpty()) return QModelIndex(); const int start = after == -1 ? items_.count() : after; const int end = start + items.count() - 1; beginInsertRows(QModelIndex(), start, end); for (int i=start ; i<=end ; ++i) { items_.insert(i, items[i - start]); virtual_items_ << virtual_items_.count(); } endInsertRows(); Save(); ReshuffleIndices(); return index(start, 0); } QModelIndex Playlist::InsertSongs(const SongList& songs, int after) { QList items; foreach (const Song& song, songs) { items << new SongPlaylistItem(song); } return InsertItems(items, after); } QModelIndex Playlist::InsertRadioStations(const QList& items, int after) { QList playlist_items; foreach (RadioItem* item, items) { if (!item->playable) continue; playlist_items << new RadioPlaylistItem(item->service, item->Url(), item->Title(), item->Artist()); } return InsertItems(playlist_items, after); } QModelIndex Playlist::InsertStreamUrls(const QList& urls, int after) { QList playlist_items; foreach (const QUrl& url, urls) { playlist_items << new RadioPlaylistItem( RadioModel::ServiceByName(SavedRadio::kServiceName), url.toString(), url.toString(), QString()); } return InsertItems(playlist_items, after); } QMimeData* Playlist::mimeData(const QModelIndexList& indexes) const { QMimeData* data = new QMimeData; QList urls; QList rows; foreach (const QModelIndex& index, indexes) { if (index.column() != 0) continue; urls << items_[index.row()]->Url(); rows << index.row(); } QBuffer buf; buf.open(QIODevice::WriteOnly); QDataStream stream(&buf); stream << rows; buf.close(); data->setUrls(urls); data->setData(kRowsMimetype, buf.data()); return data; } bool Playlist::CompareItems(int column, Qt::SortOrder order, const PlaylistItem* _a, const PlaylistItem* _b) { const PlaylistItem* a = order == Qt::AscendingOrder ? _a : _b; const PlaylistItem* b = order == Qt::AscendingOrder ? _b : _a; #define cmp(field) return a->Metadata().field() < b->Metadata().field() switch (column) { case Column_Title: cmp(title); case Column_Artist: cmp(artist); case Column_Album: cmp(album); case Column_Length: cmp(length); case Column_Track: cmp(track); case Column_Disc: cmp(disc); case Column_Year: cmp(year); case Column_Genre: cmp(genre); case Column_AlbumArtist: cmp(albumartist); case Column_Composer: cmp(composer); case Column_BPM: cmp(bpm); case Column_Bitrate: cmp(bitrate); case Column_Samplerate: cmp(samplerate); case Column_Filename: cmp(filename); 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); } #undef cmp return false; } void Playlist::sort(int column, Qt::SortOrder order) { if (ignore_sorting_) return; layoutAboutToBeChanged(); // This is a slow and nasty way to keep the persistent indices QMap old_persistent_mappings; foreach (const QModelIndex& index, persistentIndexList()) { old_persistent_mappings[index.row()] = items_[index.row()]; } qStableSort(items_.begin(), items_.end(), boost::bind(&Playlist::CompareItems, column, order, _1, _2)); QMapIterator it(old_persistent_mappings); while (it.hasNext()) { it.next(); for (int col=0 ; coltype_string()); items_.at(i)->Save(s); } s.endArray(); } void Playlist::Restore() { qDeleteAll(items_); items_.clear(); virtual_items_.clear(); QSettings s; s.beginGroup(kSettingsGroup); int count = s.beginReadArray("items"); for (int i=0 ; iRestore(s); items_ << item; virtual_items_ << virtual_items_.count(); } s.endArray(); reset(); } bool Playlist::removeRows(int row, int count, const QModelIndex& parent) { beginRemoveRows(parent, row, row+count-1); // Remove items for (int i=0 ; i::iterator it = virtual_items_.begin(); int i = 0; while (it != virtual_items_.end()) { if (*it >= items_.count()) { if (i >= current_virtual_index_) current_virtual_index_ --; it = virtual_items_.erase(it); } else { ++it; } ++i; } Save(); return true; } 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)); if (stop_after_.isValid()) emit dataChanged(stop_after_, stop_after_.sibling(stop_after_.row(), ColumnCount)); } void Playlist::SetStreamMetadata(const QUrl& url, const Song& song) { if (!current_item_.isValid()) return; PlaylistItem* item = items_[current_item_.row()]; if (item->Url() != url) return; // Don't update the metadata if it's only a minor change from before if (item->Metadata().artist() == song.artist() && item->Metadata().title() == song.title()) return; item->SetTemporaryMetadata(song); UpdateScrobblePoint(); emit dataChanged(index(current_item_.row(), 0), index(current_item_.row(), ColumnCount)); emit CurrentSongChanged(song); } void Playlist::ClearStreamMetadata() { if (!current_item_.isValid()) return; PlaylistItem* item = items_[current_item_.row()]; item->ClearTemporaryMetadata(); UpdateScrobblePoint(); emit dataChanged(index(current_item_.row(), 0), index(current_item_.row(), ColumnCount)); } bool Playlist::stop_after_current() const { return stop_after_.isValid() && current_item_.isValid() && stop_after_.row() == current_item_.row(); } PlaylistItem* Playlist::current_item() const { int i = current_index(); if (i == -1) return NULL; return item_at(i); } PlaylistItem::Options Playlist::current_item_options() const { PlaylistItem* item = current_item(); if (!item) return PlaylistItem::Default; return item->options(); } Song Playlist::current_item_metadata() const { PlaylistItem* item = current_item(); if (!item) return Song(); return item->Metadata(); } void Playlist::UpdateScrobblePoint() { int length = qMin(current_item_metadata().length(), 240); ScrobblePoint point(length / 2); scrobble_point_ = point; has_scrobbled_ = false; } void Playlist::Clear() { qDeleteAll(items_); items_.clear(); virtual_items_.clear(); reset(); Save(); } void Playlist::ReloadItems(const QList& rows) { foreach (int row, rows) { item_at(row)->Reload(); emit dataChanged(index(row, 0), index(row, ColumnCount-1)); } } void Playlist::Shuffle() { layoutAboutToBeChanged(); const int count = items_.count(); for (int i=0 ; i < count; ++i) { int new_pos = i + (rand() % (count - i)); std::swap(items_[i], items_[new_pos]); foreach (const QModelIndex& pidx, persistentIndexList()) { if (pidx.row() == i) changePersistentIndex(pidx, index(new_pos, pidx.column(), QModelIndex())); else if (pidx.row() == new_pos) changePersistentIndex(pidx, index(i, pidx.column(), QModelIndex())); } } layoutChanged(); Save(); } void Playlist::ReshuffleIndices() { if (!is_shuffled_) { std::sort(virtual_items_.begin(), virtual_items_.end()); if (current_index() != -1) current_virtual_index_ = virtual_items_.indexOf(current_index()); } else { QList::iterator begin = virtual_items_.begin(); if (current_virtual_index_ != -1) std::advance(begin, current_virtual_index_ + 1); std::random_shuffle(begin, virtual_items_.end()); } } void Playlist::set_sequence(PlaylistSequence* v) { playlist_sequence_ = v; connect(v, SIGNAL(ShuffleModeChanged(PlaylistSequence::ShuffleMode)), SLOT(ShuffleModeChanged(PlaylistSequence::ShuffleMode))); ShuffleModeChanged(v->shuffle_mode()); }