/* * Cantata * * Copyright (c) 2011-2014 Craig Drummond * Copyright (c) 2014 Niklas Wenzel * */ /* * Copyright (c) 2008 Sander Knopper (sander AT knopper DOT tk) and * Roeland Douma (roeland AT rullzer DOT com) * * This file is part of QtMPC. * * QtMPC 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 2 of the License, or * (at your option) any later version. * * QtMPC 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 QtMPC. If not, see . */ #include "support/localize.h" #include "playqueuemodel.h" #include "mpd/mpdconnection.h" #include "mpd/mpdparseutils.h" #include "mpd/mpdstats.h" #include "mpd/mpdstatus.h" #include "streams/streamfetcher.h" #include "streamsmodel.h" #include "http/httpserver.h" #include "gui/settings.h" #include "support/icon.h" #include "support/utils.h" #include "config.h" #include "support/action.h" #include "support/actioncollection.h" #include "gui/covers.h" #include "widgets/groupedview.h" #include "roles.h" #ifdef ENABLE_DEVICES_SUPPORT #include "devicesmodel.h" #if defined CDDB_FOUND || defined MUSICBRAINZ5_FOUND #include "devices/audiocddevice.h" #endif #endif #include #include #include #include #include #include #include #include #include #include #if defined ENABLE_MODEL_TEST #include "modeltest.h" #endif #ifndef ENABLE_UBUNTU const QLatin1String PlayQueueModel::constMoveMimeType("cantata/move"); const QLatin1String PlayQueueModel::constFileNameMimeType("cantata/filename"); const QLatin1String PlayQueueModel::constUriMimeType("text/uri-list"); static const char * constSortByKey="sort-by"; static const QLatin1String constSortByArtistKey("artist"); static const QLatin1String constSortByAlbumArtistKey("albumartist"); static const QLatin1String constSortByAlbumKey("album"); static const QLatin1String constSortByGenreKey("genre"); static const QLatin1String constSortByYearKey("year"); static bool checkExtension(const QString &file) { static QSet constExtensions=QSet() << QLatin1String("mp3") << QLatin1String("ogg") << QLatin1String("flac") << QLatin1String("wma") << QLatin1String("m4a") << QLatin1String("m4b") << QLatin1String("mp4") << QLatin1String("m4p") << QLatin1String("wav") << QLatin1String("wv") << QLatin1String("wvp") << QLatin1String("aiff") << QLatin1String("aif") << QLatin1String("aifc") << QLatin1String("ape") << QLatin1String("spx") << QLatin1String("tta") << QLatin1String("mpc") << QLatin1String("mpp") << QLatin1String("mp+") << QLatin1String("dff") << QLatin1String("dsf"); int pos=file.lastIndexOf('.'); return pos>1 ? constExtensions.contains(file.mid(pos+1).toLower()) : false; } static QStringList parseUrls(const QStringList &urls, bool percentEncoded) { QStringList useable; foreach (const QString &path, urls) { QUrl u=percentEncoded ? QUrl::fromPercentEncoding(path.toUtf8()) : QUrl(path); #if defined ENABLE_DEVICES_SUPPORT && (defined CDDB_FOUND || defined MUSICBRAINZ5_FOUND) QString cdDevice=AudioCdDevice::getDevice(u); if (!cdDevice.isEmpty()) { DevicesModel::self()->playCd(cdDevice); } else #endif if (QLatin1String("http")==u.scheme()) { useable.append(u.toString()); } else if ((u.scheme().isEmpty() || QLatin1String("file")==u.scheme()) && checkExtension(u.path())) { if (!HttpServer::self()->forceUsage() && MPDConnection::self()->getDetails().isLocal() && !u.path().startsWith(QLatin1String("/media/"))) { useable.append(QLatin1String("file://")+u.path()); } else if (HttpServer::self()->isAlive()) { useable.append(HttpServer::self()->encodeUrl(u.path())); } } } return useable; } void PlayQueueModel::encode(QMimeData &mimeData, const QString &mime, const QStringList &values) { QByteArray encodedData; QTextStream stream(&encodedData, QIODevice::WriteOnly); foreach (const QString &v, values) { stream << v << endl; } mimeData.setData(mime, encodedData); } void PlayQueueModel::encode(QMimeData &mimeData, const QString &mime, const QList &values) { QByteArray encodedData; QDataStream stream(&encodedData, QIODevice::WriteOnly); foreach (quint32 v, values) { stream << v; } mimeData.setData(mime, encodedData); } QStringList PlayQueueModel::decode(const QMimeData &mimeData, const QString &mime) { QByteArray encodedData=mimeData.data(mime); QTextStream stream(&encodedData, QIODevice::ReadOnly); QStringList rv; while (!stream.atEnd()) { rv.append(stream.readLine().remove('\n')); } return rv; } QList PlayQueueModel::decodeInts(const QMimeData &mimeData, const QString &mime) { QByteArray encodedData=mimeData.data(mime); QDataStream stream(&encodedData, QIODevice::ReadOnly); QList rv; while (!stream.atEnd()) { quint32 v; stream >> v; rv.append(v); } return rv; } QString PlayQueueModel::headerText(int col) { switch (col) { case COL_TITLE: return i18n("Title"); case COL_ARTIST: return i18n("Artist"); case COL_ALBUM: return i18n("Album"); case COL_TRACK: return i18n("Track"); case COL_LENGTH: return i18n("Length"); case COL_DISC: return i18n("Disc"); case COL_YEAR: return i18n("Year"); case COL_GENRE: return i18n("Genre"); case COL_PRIO: return i18n("Priority"); case COL_COMPOSER: return i18n("Composer"); case COL_PERFORMER: return i18n("Performer"); default: return QString(); } } #endif PlayQueueModel::PlayQueueModel(QObject *parent) : QAbstractItemModel(parent) , currentSongId(-1) , currentSongRowNum(-1) , time(0) , mpdState(MPDState_Inactive) , stopAfterCurrent(false) , stopAfterTrackId(-1) #ifdef ENABLE_UBUNTU , undoLimit(0) #else , undoLimit(Settings::self()->undoSteps()) #endif , undoEnabled(undoLimit>0) , lastCommand(Cmd_Other) #ifndef ENABLE_UBUNTU , dropAdjust(0) #endif { fetcher=new StreamFetcher(this); connect(this, SIGNAL(modelReset()), this, SLOT(stats())); connect(fetcher, SIGNAL(result(const QStringList &, int, bool, quint8)), SLOT(addFiles(const QStringList &, int, bool, quint8))); connect(fetcher, SIGNAL(result(const QStringList &, int, bool, quint8)), SIGNAL(streamsFetched())); connect(fetcher, SIGNAL(status(QString)), SIGNAL(streamFetchStatus(QString))); connect(this, SIGNAL(filesAdded(const QStringList, quint32, quint32, int, quint8)), MPDConnection::self(), SLOT(add(const QStringList, quint32, quint32, int, quint8))); connect(this, SIGNAL(populate(QStringList, QList)), MPDConnection::self(), SLOT(populate(QStringList, QList))); connect(this, SIGNAL(move(const QList &, quint32, quint32)), MPDConnection::self(), SLOT(move(const QList &, quint32, quint32))); connect(MPDConnection::self(), SIGNAL(prioritySet(const QList &, quint8)), SLOT(prioritySet(const QList &, quint8))); connect(MPDConnection::self(), SIGNAL(stopAfterCurrentChanged(bool)), SLOT(stopAfterCurrentChanged(bool))); connect(this, SIGNAL(stop(bool)), MPDConnection::self(), SLOT(stopPlaying(bool))); connect(this, SIGNAL(clearStopAfter()), MPDConnection::self(), SLOT(clearStopAfter())); connect(this, SIGNAL(removeSongs(QList)), MPDConnection::self(), SLOT(removeSongs(QList))); connect(this, SIGNAL(clearEntries()), MPDConnection::self(), SLOT(clear())); connect(this, SIGNAL(addAndPlay(QString)), MPDConnection::self(), SLOT(addAndPlay(QString))); connect(this, SIGNAL(startPlayingSongId(qint32)), MPDConnection::self(), SLOT(startPlayingSongId(qint32))); #ifdef ENABLE_DEVICES_SUPPORT //TODO: Problems here with devices support!!! connect(DevicesModel::self(), SIGNAL(invalid(QList)), SLOT(remove(QList))); connect(DevicesModel::self(), SIGNAL(updatedDetails(QList)), SLOT(updateDetails(QList))); #endif #if defined ENABLE_MODEL_TEST new ModelTest(this, this); #endif #ifdef ENABLE_UBUNTU connect(Covers::self(), SIGNAL(cover(const Song &, const QImage &, const QString &)), this, SLOT(setCover(const Song &, const QImage &, const QString &))); #else #ifdef ENABLE_KDE_SUPPORT bool menuIcons=true; #else bool menuIcons=!QCoreApplication::testAttribute(Qt::AA_DontShowIconsInMenus); #endif removeDuplicatesAction=new Action(i18n("Remove Duplicates"), this); removeDuplicatesAction->setEnabled(false); undoAction=ActionCollection::get()->createAction("playqueue-undo", i18n("Undo"), menuIcons ? "edit-undo" : ""); undoAction->setShortcut(Qt::ControlModifier+Qt::Key_Z); redoAction=ActionCollection::get()->createAction("playqueue-redo", i18n("Redo"), menuIcons ? "edit-redo" : ""); redoAction->setShortcut(Qt::ControlModifier+Qt::ShiftModifier+Qt::Key_Z); connect(undoAction, SIGNAL(triggered(bool)), this, SLOT(undo())); connect(redoAction, SIGNAL(triggered(bool)), this, SLOT(redo())); connect(removeDuplicatesAction, SIGNAL(triggered(bool)), this, SLOT(removeDuplicates())); shuffleAction=new Action(i18n("Shuffle"), this); shuffleAction->setMenu(new QMenu(0)); Action *shuffleTracksAction = new Action(i18n("Tracks"), shuffleAction); Action *shuffleAlbumsAction = new Action(i18n("Albums"), shuffleAction); connect(shuffleTracksAction, SIGNAL(triggered(bool)), MPDConnection::self(), SLOT(shuffle())); connect(shuffleAlbumsAction, SIGNAL(triggered(bool)), this, SLOT(shuffleAlbums())); shuffleAction->menu()->addAction(shuffleTracksAction); shuffleAction->menu()->addAction(shuffleAlbumsAction); sortAction=new Action(i18n("Sort By"), this); sortAction->setMenu(new QMenu(0)); addSortAction(i18n("Artist"), constSortByArtistKey); addSortAction(i18n("Album Artist"), constSortByAlbumArtistKey); addSortAction(i18n("Album"), constSortByAlbumKey); addSortAction(i18n("Genre"), constSortByGenreKey); addSortAction(i18n("Year"), constSortByYearKey); controlActions(); shuffleAction->setEnabled(false); sortAction->setEnabled(false); #endif } PlayQueueModel::~PlayQueueModel() { } QModelIndex PlayQueueModel::index(int row, int column, const QModelIndex &parent) const { return hasIndex(row, column, parent) ? createIndex(row, column, (void *)&songs.at(row)) : QModelIndex(); } QModelIndex PlayQueueModel::parent(const QModelIndex &idx) const { Q_UNUSED(idx) return QModelIndex(); } #ifndef ENABLE_UBUNTU QVariant PlayQueueModel::headerData(int section, Qt::Orientation orientation, int role) const { if (Qt::Horizontal==orientation) { switch (role) { case Qt::DisplayRole: return headerText(section); case Qt::TextAlignmentRole: switch (section) { case COL_TITLE: case COL_ARTIST: case COL_ALBUM: case COL_GENRE: case COL_COMPOSER: case COL_PERFORMER: default: return int(Qt::AlignVCenter|Qt::AlignLeft); case COL_TRACK: case COL_LENGTH: case COL_DISC: case COL_YEAR: case COL_PRIO: return int(Qt::AlignVCenter|Qt::AlignRight); } case Cantata::Role_Hideable: return COL_YEAR==section || COL_DISC==section || COL_GENRE==section || COL_PRIO==section || COL_COMPOSER==section || COL_PERFORMER==section ? true : false; case Cantata::Role_Width: switch (section) { case COL_TRACK: return 0.075; case COL_DISC: return 0.03; case COL_TITLE: return 0.3; case COL_ARTIST: return 0.27; case COL_ALBUM: return 0.27; case COL_LENGTH: return 0.05; case COL_YEAR: return 0.05; case COL_GENRE: return 0.1; case COL_PRIO: return 0.015; case COL_COMPOSER: return 0.2; case COL_PERFORMER: return 0.2; } default: break; } } return QVariant(); } #endif int PlayQueueModel::rowCount(const QModelIndex &idx) const { return idx.isValid() ? 0 : songs.size(); } static QString basicPath(const Song &song) { #ifdef ENABLE_HTTP_SERVER if (song.isCantataStream()) { Song mod=HttpServer::self()->decodeUrl(song.file); if (!mod.file.isEmpty()) { return mod.file; } } #endif QString path=song.filePath(); int marker=path.indexOf(QLatin1Char('#')); return -1==marker ? path : path.left(marker); } #ifdef ENABLE_UBUNTU static const QString constDefaultCover=QLatin1String("qrc:/album.svg"); //Expose role names, so that they can be accessed via QML QHash PlayQueueModel::roleNames() const { QHash roles; roles[Cantata::Role_MainText] = "mainText"; roles[Cantata::Role_SubText] = "subText"; roles[Cantata::Role_Image] = "image"; roles[Cantata::Role_Time] = "time"; return roles; } #endif QVariant PlayQueueModel::data(const QModelIndex &index, int role) const { if (Qt::SizeHintRole!=role && (!index.isValid() || index.row() >= songs.size())) { return QVariant(); } // Mark background of song currently being played // if (role == Qt::BackgroundRole && songs.at(index.row()).id == currentSongId) { // QPalette palette; // QColor col(palette.color(QPalette::Highlight)); // col.setAlphaF(0.2); // return QVariant(col); // } switch (role) { case Cantata::Role_MainText: { const Song &s=songs.at(index.row()); return s.title.isEmpty() ? s.file : s.trackAndTitleStr(false); } case Cantata::Role_SubText: { const Song &s=songs.at(index.row()); return s.artist+QLatin1String(" - ")+s.displayAlbum(); } case Cantata::Role_Time: { const Song &s=songs.at(index.row()); return s.time>0 ? Utils::formatTime(s.time) : QLatin1String(""); } #ifdef ENABLE_UBUNTU case Cantata::Role_Image: { Song s=songs.at(index.row()); QMap::ConstIterator it=covers.find(s.key); if (it!=covers.constEnd()) { return it.value().isEmpty() ? constDefaultCover : it.value(); } QString fileName=Covers::self()->requestImage(s).fileName; covers.insert(s.key, fileName); if (fileName.isEmpty()) { coverRequests.insert(s.key, s); return constDefaultCover; } return fileName; } #endif case Cantata::Role_IsCollection: return false; case Cantata::Role_CollectionId: return 0; case Cantata::Role_Key: return songs.at(index.row()).key; case Cantata::Role_Id: return songs.at(index.row()).id; case Cantata::Role_Song: { QVariant var; var.setValue(songs.at(index.row())); return var; } case Cantata::Role_AlbumDuration: { const Song &first = songs.at(index.row()); quint32 d=first.time; for (int i=index.row()+1; i1) { for (int i=index.row()-1; i<=0; ++i) { const Song &song = songs.at(i); if (song.key!=first.key) { break; } d+=song.time; } } return d; } case Cantata::Role_SongCount: { const Song &first = songs.at(index.row()); quint32 count=1; for (int i=index.row()+1; i1) { for (int i=index.row()-1; i<=0; ++i) { const Song &song = songs.at(i); if (song.key!=first.key) { break; } count++; } } return count; } case Cantata::Role_CurrentStatus: { quint16 key=songs.at(index.row()).key; for (int i=index.row()+1; i")+ s.displayAlbum()+QLatin1String("
")+ s.trackAndTitleStr()+QLatin1String("
")+ Utils::formatTime(s.time)+QLatin1String("
")+ (s.priority>0 ? i18n("(Priority: %1)", s.priority)+QLatin1String("
") : QString())+ QLatin1String("")+basicPath(s)+QLatin1String(""); } } case Qt::TextAlignmentRole: switch (index.column()) { case COL_TITLE: case COL_ARTIST: case COL_ALBUM: case COL_GENRE: case COL_COMPOSER: case COL_PERFORMER: default: return int(Qt::AlignVCenter|Qt::AlignLeft); case COL_TRACK: case COL_LENGTH: case COL_DISC: case COL_YEAR: case COL_PRIO: return int(Qt::AlignVCenter|Qt::AlignRight); } #ifndef ENABLE_UBUNTU case Cantata::Role_Decoration: { qint32 id=songs.at(index.row()).id; if (id==currentSongId) { switch (mpdState) { case MPDState_Inactive: case MPDState_Stopped: return Icon("media-playback-stop"); case MPDState_Playing: return Icon(stopAfterCurrent ? "media-playback-stop" : "media-playback-start"); case MPDState_Paused: return Icon("media-playback-pause"); } } else if (-1!=id && id==stopAfterTrackId) { return Icon("media-playback-stop"); } break; } #endif default: break; } return QVariant(); } #ifndef ENABLE_UBUNTU bool PlayQueueModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (Cantata::Role_DropAdjust==role) { dropAdjust=value.toUInt(); return true; } else { return QAbstractItemModel::setData(index, value, role); } } #endif Qt::DropActions PlayQueueModel::supportedDropActions() const { return Qt::CopyAction | Qt::MoveAction; } Qt::ItemFlags PlayQueueModel::flags(const QModelIndex &index) const { if (index.isValid()) { return Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsEnabled; } return Qt::ItemIsDropEnabled; } #ifndef ENABLE_UBUNTU /** * @return A QStringList with the mimetypes we support */ QStringList PlayQueueModel::mimeTypes() const { return QStringList() << constMoveMimeType << constFileNameMimeType << constUriMimeType; } /** * Convert the data at indexes into mimedata ready for transport * * @param indexes The indexes to pack into mimedata * @return The mimedata */ QMimeData *PlayQueueModel::mimeData(const QModelIndexList &indexes) const { QMimeData *mimeData = new QMimeData(); QList positions; QStringList filenames; foreach(QModelIndex index, indexes) { if (index.isValid() && 0==index.column()) { positions.append(index.row()); filenames.append(static_cast(index.internalPointer())->file); } } encode(*mimeData, constMoveMimeType, positions); encode(*mimeData, constFileNameMimeType, filenames); return mimeData; } /** * Act on mime data that is dropped in our model * * @param data The actual data that is dropped * @param action The action. This could mean drop/copy etc * @param row The row where it is dropper * @param column The column where it is dropper * @param parent The parent of where we have dropped it * * @return bool if we accest the drop */ bool PlayQueueModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int /*column*/, const QModelIndex & /*parent*/) { if (Qt::IgnoreAction==action) { return true; } row+=dropAdjust; if (data->hasFormat(constMoveMimeType)) { //Act on internal moves if (row < 0) { emit move(decodeInts(*data, constMoveMimeType), songs.size(), songs.size()); } else { emit move(decodeInts(*data, constMoveMimeType), row, songs.size()); } return true; } else if (data->hasFormat(constFileNameMimeType)) { //Act on moves from the music library and dir view addItems(decode(*data, constFileNameMimeType), row, false, 0); return true; } else if(data->hasFormat(constUriMimeType)/* && MPDConnection::self()->getDetails().isLocal()*/) { QStringList useable=parseUrls(decode(*data, constUriMimeType), true); if (useable.count()) { addItems(useable, row, false, 0); return true; } } return false; } #endif void PlayQueueModel::load(const QStringList &urls) { #ifdef ENABLE_UBUNTU Q_UNUSED(urls) #else QStringList useable=parseUrls(urls, false); if (useable.count()) { addItems(useable, songs.count(), songs.isEmpty(), 0); } #endif } void PlayQueueModel::addItems(const QStringList &items, int row, bool replace, quint8 priority) { bool haveRadioStream=false; foreach (const QString &f, items) { QUrl u(f); if (u.scheme().startsWith(StreamsModel::constPrefix)) { haveRadioStream=true; break; } } if (haveRadioStream) { emit fetchingStreams(); fetcher->get(items, row, replace, priority); } else { addFiles(items, row, replace, priority); } } void PlayQueueModel::addFiles(const QStringList &filenames, int row, bool replace, quint8 priority) { if (replace) { emit filesAdded(filenames, 0, 0, MPDConnection::AddReplaceAndPlay, priority); } else if (songs.isEmpty()) { emit filesAdded(filenames, 0, 0, MPDConnection::AddToEnd, priority); } else if (row < 0) { emit filesAdded(filenames, songs.size(), songs.size(), MPDConnection::AddToEnd, priority); } else { emit filesAdded(filenames, row, songs.size(), MPDConnection::AddToEnd, priority); } } void PlayQueueModel::prioritySet(const QList &ids, quint8 priority) { QList prev; if (undoEnabled) { foreach (Song s, songs) { prev.append(s); } } QSet i=ids.toSet(); int row=0; foreach (const Song &s, songs) { if (i.contains(s.id)) { s.priority=priority; i.remove(s.id); QModelIndex idx(index(row, 0)); emit dataChanged(idx, idx); if (i.isEmpty()) { break; } } ++row; } saveHistory(prev); } qint32 PlayQueueModel::getIdByRow(qint32 row) const { return row>=songs.size() ? -1 : songs.at(row).id; } qint32 PlayQueueModel::getSongId(const QString &file) const { foreach (const Song &s, songs) { if (s.file==file) { return s.id; } } return -1; } // qint32 PlayQueueModel::getPosByRow(qint32 row) const // { // return row>=songs.size() ? -1 : songs.at(row).pos; // } qint32 PlayQueueModel::getRowById(qint32 id) const { for (int i = 0; i < songs.size(); i++) { if (songs.at(i).id == id) { return i; } } return -1; } Song PlayQueueModel::getSongByRow(const qint32 row) const { return row<0 || row>=songs.size() ? Song() : songs.at(row); } Song PlayQueueModel::getSongById(qint32 id) const { foreach (const Song &s, songs) { if (s.id==id) { return s; } } return Song(); } void PlayQueueModel::updateCurrentSong(quint32 id) { qint32 oldIndex = currentSongId; currentSongId = id; if (-1!=oldIndex) { int row=-1==currentSongRowNum ? getRowById(oldIndex) : currentSongRowNum; emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex())-1)); } currentSongRowNum=getRowById(currentSongId); emit dataChanged(index(currentSongRowNum, 0), index(currentSongRowNum, columnCount(QModelIndex())-1)); if (-1!=currentSongId && stopAfterTrackId==currentSongId) { stopAfterTrackId=-1; emit stop(true); } } void PlayQueueModel::clear() { beginResetModel(); songs.clear(); ids.clear(); currentSongId=-1; currentSongRowNum=0; stopAfterTrackId=-1; time=0; endResetModel(); } qint32 PlayQueueModel::currentSongRow() const { if (-1==currentSongRowNum) { currentSongRowNum=getRowById(currentSongId); } return currentSongRowNum; } void PlayQueueModel::setState(MPDState st) { if (st!=mpdState) { mpdState=st; if (-1!=currentSongId) { if (-1==currentSongRowNum) { currentSongRowNum=getRowById(currentSongId); } emit dataChanged(index(currentSongRowNum, 0), index(currentSongRowNum, 2)); } } } // Update playqueue with contents returned from MPD. void PlayQueueModel::update(const QList &songList) { if (songList.isEmpty()) { Song::clearKeyStore(MPDParseUtils::Loc_PlayQueue); #ifdef ENABLE_UBUNTU covers.clear(); coverRequests.clear(); #endif } #ifndef ENABLE_UBUNTU removeDuplicatesAction->setEnabled(songList.count()>1); #endif QList prev; if (undoEnabled) { prev=songs; } QSet newIds; #ifdef ENABLE_UBUNTU QSet currentKeys; #endif foreach (const Song &s, songList) { newIds.insert(s.id); } if (songs.isEmpty() || songList.isEmpty()) { beginResetModel(); songs=songList; ids=newIds; endResetModel(); if (songList.isEmpty()) { stopAfterTrackId=-1; } #ifdef ENABLE_UBUNTU foreach (const Song &s, songs) { currentKeys.insert(s.key); } #endif } else { time = 0; QSet removed=ids-newIds; foreach (qint32 id, removed) { qint32 row=getRowById(id); if (row!=-1) { beginRemoveRows(QModelIndex(), row, row); songs.removeAt(row); endRemoveRows(); } } for (qint32 i=0; i=songs.count(); Song curentSongAtPos=newSong ? Song() : songs.at(i); bool isEmpty=s.isEmpty(); if (newSong || s.id!=curentSongAtPos.id) { qint32 existingPos=newSong ? -1 : getRowById(s.id); if (-1==existingPos) { beginInsertRows(QModelIndex(), i, i); songs.insert(i, s); #ifdef ENABLE_UBUNTU currentKeys.insert(s.key); #endif endInsertRows(); } else { beginMoveRows(QModelIndex(), existingPos, existingPos, QModelIndex(), i>existingPos ? i+1 : i); Song old=songs.takeAt(existingPos); // old.pos=s.pos; songs.insert(i, isEmpty ? old : s); #ifdef ENABLE_UBUNTU currentKeys.insert(isEmpty ? old.key : s.key); #endif endMoveRows(); } } else if (isEmpty) { s=curentSongAtPos; #ifdef ENABLE_UBUNTU currentKeys.insert(s.key); #endif } else { s.key=curentSongAtPos.key; #ifdef ENABLE_UBUNTU currentKeys.insert(s.key); #endif songs.replace(i, s); if (s.name!=curentSongAtPos.name || s.title!=curentSongAtPos.title || s.artist!=curentSongAtPos.artist) { emit dataChanged(index(i, 0), index(i, columnCount(QModelIndex())-1)); } } time += s.time; } if (songs.count()>songList.count()) { int toBeRemoved=songs.count()-songList.count(); beginRemoveRows(QModelIndex(), songList.count(), songs.count()-1); for (int i=0; i removedKeys=covers.keys().toSet()-currentKeys; if (!removedKeys.isEmpty()) { foreach (quint16 k, removedKeys) { covers.remove(k); coverRequests.remove(k); } } #else shuffleAction->setEnabled(songs.count()>1); sortAction->setEnabled(songs.count()>1); #endif } void PlayQueueModel::setStopAfterTrack(qint32 track) { bool clear=track==stopAfterTrackId || (track==currentSongId && stopAfterCurrent); stopAfterTrackId=clear ? -1 : track; qint32 row=getRowById(track); if (-1!=row) { emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex())-1)); } if (clear) { emit clearStopAfter(); } else if (stopAfterTrackId==currentSongId) { emit stop(true); } } bool PlayQueueModel::removeCantataStreams() { QList ids; foreach (const Song &s, songs) { if (s.isCantataStream() || s.isCdda()) { ids.append(s.id); } } if (!ids.isEmpty()) { emit removeSongs(ids); return true; } return false; } void PlayQueueModel::removeAll() { emit clearEntries(); } void PlayQueueModel::remove(const QList &rowsToRemove) { QList removeIds; foreach (const int &r, rowsToRemove) { if (r>-1 && r &rowsToKeep) { QSet allIds; foreach(const Song &song, songs) { allIds.insert(song.id); } QSet keepIds; foreach (const int &r, rowsToKeep) { if (r>-1 && r removeIds=allIds-keepIds; if (!removeIds.isEmpty()) { emit removeSongs(removeIds.toList()); } } void PlayQueueModel::enableUndo(bool e) { if (e==undoEnabled) { return; } undoEnabled=e && undoLimit>0; if (!e) { undoStack.clear(); redoStack.clear(); } #ifndef ENABLE_UBUNTU controlActions(); #endif } static PlayQueueModel::UndoItem getState(const QList &songs) { PlayQueueModel::UndoItem item; foreach (const Song &s, songs) { item.files.append(s.file); item.priority.append(s.priority); } return item; } static bool equalSongList(const QList &a, const QList &b) { if (a.count()!=b.count()) { return false; } for (int i=0; i &prevList) { if (!undoEnabled) { return; } if (equalSongList(prevList, songs)) { lastCommand=Cmd_Other; return; } switch (lastCommand) { case Cmd_Redo: { if (redoStack.isEmpty()) { lastCommand=Cmd_Other; } else { UndoItem actioned=redoStack.pop(); if (actioned!=getState(songs)) { lastCommand=Cmd_Other; } else { undoStack.push(getState(prevList)); } } break; } case Cmd_Undo: { if (undoStack.isEmpty()) { lastCommand=Cmd_Other; } else { UndoItem actioned=undoStack.pop(); if (actioned!=getState(songs)) { lastCommand=Cmd_Other; } else { redoStack.push(getState(prevList)); } } break; } case Cmd_Other: break; } if (Cmd_Other==lastCommand) { redoStack.clear(); undoStack.push(getState(prevList)); if (undoStack.size()>undoLimit) { undoStack.pop_back(); } } #ifndef ENABLE_UBUNTU controlActions(); #endif lastCommand=Cmd_Other; } #ifndef ENABLE_UBUNTU void PlayQueueModel::controlActions() { undoAction->setEnabled(!undoStack.isEmpty()); undoAction->setVisible(undoLimit>0); redoAction->setEnabled(!redoStack.isEmpty()); redoAction->setVisible(undoLimit>0); } void PlayQueueModel::addSortAction(const QString &name, const QString &key) { Action *action=new Action(name, sortAction); action->setProperty(constSortByKey, key); sortAction->menu()->addAction(action); connect(action, SIGNAL(triggered(bool)), SLOT(sortBy())); } static bool artistSort(const Song &s1, const Song &s2) { int c=s1.artist.localeAwareCompare(s2.artist); return c<0 || (c==0 && s1(sender()); if (act) { QString key=act->property(constSortByKey).toString(); QList copy=songs; if (constSortByArtistKey==key) { qSort(copy.begin(), copy.end(), artistSort); } else if (constSortByAlbumArtistKey==key) { qSort(copy.begin(), copy.end(), albumArtistSort); } else if (constSortByAlbumKey==key) { qSort(copy.begin(), copy.end(), albumSort); } else if (constSortByGenreKey==key) { qSort(copy.begin(), copy.end(), genreSort); } else if (constSortByYearKey==key) { qSort(copy.begin(), copy.end(), yearSort); } if (copy!=songs) { QStringList files; foreach (const Song &s, copy) { files.append(s.file); } emit filesAdded(files, 0, 0, MPDState_Playing==MPDStatus::self()->state() ? MPDConnection::AddReplaceAndPlay : MPDConnection::AddAndReplace , 0); } } #endif } void PlayQueueModel::removeDuplicates() { QMap > map; foreach (const Song &song, songs) { map[song.artistSong()].append(song); } QList toRemove; foreach (const QString &key, map.keys()) { QList values=map.value(key); if (values.size()>1) { Song::sortViaType(values); for (int i=1; i keys; QMap::ConstIterator it=coverRequests.constBegin(); QMap::ConstIterator end=coverRequests.constEnd(); for (; it!=end; ++it) { if (song.albumArtist()==it.value().albumArtist() && song.album==it.value().album) { keys.append(it.key()); } } foreach (quint16 key, keys) { coverRequests.remove(key); QMap::ConstIterator it=covers.find(key); if (it!=covers.end() && it.value().isEmpty()) { coverRequests.remove(it.key()); covers[key]="file://"+file; int start=-1; int end=-1; for (int i=0; icancel(); } void PlayQueueModel::shuffleAlbums() { QMap > albums; foreach (const Song &s, songs) { albums[s.key].append(s); } QList keys=albums.keys(); if (keys.count()<2) { return; } QStringList files; while (!keys.isEmpty()) { quint32 key=keys.takeAt(Utils::random(keys.count())); QList albumSongs=albums[key]; qSort(albumSongs); foreach (const Song &song, albumSongs) { files.append(song.file); } } emit filesAdded(files, 0, 0, MPDState_Playing==MPDStatus::self()->state() ? MPDConnection::AddReplaceAndPlay : MPDConnection::AddAndReplace , 0); } void PlayQueueModel::stopAfterCurrentChanged(bool afterCurrent) { if (afterCurrent!=stopAfterCurrent) { stopAfterCurrent=afterCurrent; emit dataChanged(index(currentSongRowNum, 0), index(currentSongRowNum, columnCount(QModelIndex())-1)); } } void PlayQueueModel::remove(const QList &rem) { QSet s; QList ids; foreach (const Song &song, rem) { s.insert(song.file); } foreach (const Song &song, songs) { if (s.contains(song.file)) { ids.append(song.id); s.remove(song.file); if (s.isEmpty()) { break; } } } if (!ids.isEmpty()) { emit removeSongs(ids); } } void PlayQueueModel::updateDetails(const QList &updated) { QMap songMap; QList updatedRows; bool currentUpdated=false; Song currentSong; foreach (const Song &song, updated) { songMap[song.file]=song; } for (int i=0; i