/* * Cantata * * Copyright (c) 2011-2013 Craig Drummond * */ /* * 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 #include #include #include #include #include #include #include #include #include "localize.h" #include "playqueuemodel.h" #include "groupedview.h" #include "mpdconnection.h" #include "mpdparseutils.h" #include "mpdstats.h" #include "mpdstatus.h" #include "streamfetcher.h" #include "streamsmodel.h" #include "httpserver.h" #include "settings.h" #include "icon.h" #include "utils.h" #include "config.h" #ifdef ENABLE_DEVICES_SUPPORT #include "devicesmodel.h" #endif #if defined ENABLE_MODEL_TEST #include "modeltest.h" #endif const QLatin1String PlayQueueModel::constMoveMimeType("cantata/move"); const QLatin1String PlayQueueModel::constFileNameMimeType("cantata/filename"); const QLatin1String PlayQueueModel::constUriMimeType("text/uri-list"); 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; } 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); } 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; } QString PlayQueueModel::headerText(int col) { switch (col) { case COL_STATUS: return QString(); case COL_TITLE: return i18n("Title"); case COL_ARTIST: return i18n("Artist"); case COL_ALBUM: return i18n("Album"); case COL_TRACK: return i18nc("Track Number (#)", "#"); 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"); default: return QString(); } } PlayQueueModel::PlayQueueModel(QObject *parent) : QAbstractItemModel(parent) , currentSongId(-1) , currentSongRowNum(-1) , mpdState(MPDState_Inactive) , dropAdjust(0) , stopAfterCurrent(false) , stopAfterTrackId(-1) { 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(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))); #ifdef ENABLE_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 } 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(); } QVariant PlayQueueModel::headerData(int section, Qt::Orientation orientation, int role) const { if (Qt::Horizontal==orientation) { if (Qt::DisplayRole==role) { return headerText(section); } else if (Qt::TextAlignmentRole==role) { switch (section) { case COL_TITLE: case COL_ARTIST: case COL_ALBUM: case COL_GENRE: default: return int(Qt::AlignVCenter|Qt::AlignLeft); case COL_STATUS: case COL_TRACK: case COL_LENGTH: case COL_DISC: case COL_YEAR: case COL_PRIO: return int(Qt::AlignVCenter|Qt::AlignRight); } } } return QVariant(); } int PlayQueueModel::rowCount(const QModelIndex &idx) const { return idx.isValid() ? 0 : songs.size(); } 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 GroupedView::Role_IsCollection: return false; case GroupedView::Role_CollectionId: return 0; case GroupedView::Role_Key: return songs.at(index.row()).key; case GroupedView::Role_Id: return songs.at(index.row()).id; case GroupedView::Role_Song: { QVariant var; var.setValue(songs.at(index.row())); return var; } case GroupedView::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 GroupedView::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 GroupedView::Role_CurrentStatus: { quint16 key=songs.at(index.row()).key; for (int i=index.row()+1; i")+ s.album+(s.year>0 ? (QLatin1String(" (")+QString::number(s.year)+QChar(')')) : QString())+QLatin1String("
")+ s.trackAndTitleStr(Song::isVariousArtists(s.albumArtist()))+QLatin1String("
")+ Song::formattedTime(s.time)+QLatin1String("
")+ (s.priority>0 ? i18n("(Priority: %1)", s.priority)+QLatin1String("
") : QString())+ QLatin1String("")+s.file+QLatin1String(""); } } case Qt::TextAlignmentRole: switch (index.column()) { case COL_TITLE: case COL_ARTIST: case COL_ALBUM: case COL_GENRE: default: return int(Qt::AlignVCenter|Qt::AlignLeft); case COL_STATUS: case COL_TRACK: case COL_LENGTH: case COL_DISC: case COL_YEAR: case COL_PRIO: return int(Qt::AlignVCenter|Qt::AlignRight); } case Qt::DecorationRole: if (COL_STATUS==index.column()) { 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; case Qt::SizeHintRole: { static int sz=-1; if (-1==sz) { sz=Icon::stdSize(QApplication::fontMetrics().height()*1.2)*1.125; } return QSize(sz, sz); } default: break; } return QVariant(); } bool PlayQueueModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (GroupedView::Role_DropAdjust==role) { dropAdjust=value.toUInt(); return true; } else { return QAbstractItemModel::setData(index, value, role); } } 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; } /** * @return A QStringList with the mimetypes we support */ QStringList PlayQueueModel::mimeTypes() const { QStringList types; types << constMoveMimeType; types << constFileNameMimeType; if (HttpServer::self()->isAlive()) { types << constUriMimeType; } return types; } /** * 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(); QStringList positions; QStringList filenames; /* * Loop over all our indexes. However we have rows*columns indexes * We pack per row so ingore the columns */ QList rows; foreach(QModelIndex index, indexes) { if (index.isValid()) { if (rows.contains(index.row())) { continue; } positions.append(QString::number(index.row())); // getPosByRow(index.row()))); rows.append(index.row()); filenames.append(songs.at(index.row()).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 QStringList positions=decode(*data, constMoveMimeType); QList items; foreach (const QString &s, positions) { items.append(s.toUInt()); } if (row < 0) { emit move(items, songs.size(), songs.size()); } else { emit move(items, 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 orig=decode(*data, constUriMimeType); QStringList useable; bool haveHttp=HttpServer::self()->isAlive(); foreach (QString u, orig) { if (u.startsWith(QLatin1String("http://"))) { useable.append(u); } else if (haveHttp && (u.startsWith('/') || u.startsWith(QLatin1String("file://")))) { if (u.startsWith(QLatin1String("file://"))) { u=u.mid(7); } if (checkExtension(u)) { useable.append(HttpServer::self()->encodeUrl(QUrl::fromPercentEncoding(u.toUtf8()))); } } } if (useable.count()) { addItems(useable, row, false, 0); return true; } } return false; } 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) { //Check for empty playlist if (replace || songs.isEmpty()) { emit filesAdded(filenames, 0, 0, MPDConnection::AddReplaceAndPlay, 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) { 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()) { return; } } ++row; } } 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; 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) { QSet newIds; 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; } } else { quint32 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); 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); endMoveRows(); } } else if (isEmpty) { s=curentSongAtPos; } else { s.key=curentSongAtPos.key; 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 ids; foreach (const Song &s, songs) { if (s.isCantataStream()) { ids.append(s.id); } } if (!ids.isEmpty()) { emit removeSongs(ids); } } void PlayQueueModel::stats() { quint32 time = 0; //Loop over all songs foreach(const Song &song, songs) { time += song.time; } emit statsUpdated(songs.size(), time); } void PlayQueueModel::cancelStreamFetch() { fetcher->cancel(); } void PlayQueueModel::shuffleAlbums() { QMap albums; foreach (const Song &s, songs) { albums[s.key].append(s.file); } QList keys=albums.keys(); if (keys.count()<2) { return; } QStringList files; while (!keys.isEmpty()) { quint32 key=keys.takeAt(Utils::random(keys.count())); foreach (const QString &file, albums[key]) { files.append(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 PlayQueueModel::getSongIdSet() { QSet ids; foreach(const Song &song, songs) { ids << song.id; } return ids; } QStringList PlayQueueModel::filenames() { QStringList names; foreach(const Song &song, songs) { names << song.file; } return names; }