/* * Cantata * * Copyright (c) 2011-2018 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 "playqueuemodel.h" #include "mpd-interface/mpdparseutils.h" #include "mpd-interface/mpdstats.h" #include "mpd-interface/cuefile.h" #include "streams/streamfetcher.h" #include "streamsmodel.h" #include "http/httpserver.h" #include "gui/settings.h" #include "support/icon.h" #include "support/monoicon.h" #include "support/utils.h" #include "config.h" #include "support/action.h" #include "support/actioncollection.h" #include "support/globalstatic.h" #include "gui/covers.h" #include "widgets/groupedview.h" #include "widgets/icons.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 #include #include #include #include GLOBAL_STATIC(PlayQueueModel, instance) 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 const QLatin1String constSortByComposerKey("composer"); static const QLatin1String constSortByPerformerKey("performer"); static const QLatin1String constSortByTitleKey("title"); static const QLatin1String constSortByNumberKey("track"); static QSet constM3uPlaylists = QSet() << QLatin1String("m3u") << QLatin1String("m3u8"); static const QString constPlsPlaylist = QLatin1String("pls"); static const QString constXspfPlaylist = QLatin1String("xspf"); QSet PlayQueueModel::constFileExtensions = 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") << QLatin1String("opus") // And playlists... << QLatin1String("m3u") << QLatin1String("m3u8") << constPlsPlaylist << constXspfPlaylist; static inline QString getExtension(const QString &file) { int pos=file.lastIndexOf('.'); return pos>0 ? file.mid(pos+1).toLower() : QString(); } static inline bool checkExtension(const QString &file, const QSet &exts = PlayQueueModel::constFileExtensions) { return exts.contains(getExtension(file)); } static QString fileUrl(const QString &file, bool useServer, bool useLocal) { if (useServer) { QByteArray path = HttpServer::self()->encodeUrl(file); if (!path.isEmpty()) { return path; } } else if (useLocal && !file.startsWith(QLatin1String("/media/"))) { return QLatin1String("file://")+file; } return QString(); } static QString checkUrl(const QString &url, const QDir &dir, bool useServer, bool useLocal, const QSet &handlers) { int pos = url.indexOf(QLatin1String("://")); QString handler = pos>0 ? url.left(pos+3).toLower() : QString(); if (!handler.isEmpty() && (QLatin1String("http://")==handler || QLatin1String("https://")==handler)) { // Radio stream? return StreamsModel::constPrefix+url; } else if (handlers.contains(handler)) { return url; } else if (checkExtension(url)) { QString path = dir.filePath(url); if (QFile::exists(path)) { // Relative return fileUrl(path, useServer, useLocal); } else if (QFile::exists(url)) { // Absolute return fileUrl(url, useServer, useLocal); } } return QString(); } static QStringList expandM3uPlaylist(const QString &playlist, bool useServer, bool useLocal, const QSet &handlers) { QStringList files; QFile f(playlist); QDir dir(Utils::getDir(playlist)); if (f.open(QIODevice::ReadOnly|QIODevice::Text)) { QTextStream in(&f); while (!in.atEnd()) { QString line = in.readLine(); if (!line.startsWith(QLatin1Char('#'))) { QString url = checkUrl(line, dir, useServer, useLocal, handlers); if (!url.isEmpty()) { files.append(url); } } } f.close(); } return files; } static QStringList expandPlsPlaylist(const QString &playlist, bool useServer, bool useLocal, const QSet &handlers) { QStringList files; QFile f(playlist); QDir dir(Utils::getDir(playlist)); QMap titles; QMap urls; if (f.open(QIODevice::ReadOnly|QIODevice::Text)) { QTextStream in(&f); while (!in.atEnd()) { QString line = in.readLine(); if (line.startsWith(QLatin1String("File"))) { QStringList parts=line.split("="); if (2==parts.length()) { QString path=parts[1].trimmed(); unsigned int num=parts[0].left(4).toUInt(); QString url=checkUrl(path, dir, useServer, useLocal, handlers); if (!url.isEmpty()) { urls.insert(num, url); } } } else if (line.startsWith(QLatin1String("Title"))) { QStringList parts=line.split("="); if (2==parts.length()) { titles.insert(parts[0].left(5).toUInt(), parts[1].trimmed()); } } } f.close(); } auto it = urls.constBegin(); auto end = urls.constEnd(); for (; it!=end; ++it) { if (titles.contains(it.key()) && (it.value().startsWith(QLatin1String("http://")) || it.value().startsWith("https://"))) { files.append(it.value()+"#"+titles[it.key()]); } else { files.append(it.value()); } } return files; } static QStringList expandXspfPlaylist(const QString &playlist, bool useServer, bool useLocal, const QSet &handlers) { QStringList files; QFile f(playlist); if (f.open(QIODevice::ReadOnly|QIODevice::Text)) { QXmlStreamReader reader(&f); QDir dir(Utils::getDir(playlist)); while (!reader.atEnd()) { reader.readNext(); if (QXmlStreamReader::StartElement==reader.tokenType() && QLatin1String("location")==reader.name()) { QString url=checkUrl(reader.readElementText().trimmed(), dir, useServer, useLocal, handlers); if (!url.isEmpty()) { files.append(url); } } } f.close(); } return files; } static QStringList listFiles(const QDir &d, bool useServer, bool useLocal, const QSet &handlers, int level=5) { QStringList files; if (level) { for (const auto &f: d.entryInfoList(QDir::Files|QDir::Dirs|QDir::NoDotAndDotDot|QDir::NoSymLinks, QDir::LocaleAware|QDir::IgnoreCase)) { if (f.isDir()) { files += listFiles(QDir(f.absoluteFilePath()), useServer, useLocal, handlers, level-1); } else if (checkExtension(f.fileName(), constM3uPlaylists)) { files += expandM3uPlaylist(f.absoluteFilePath(), useServer, useLocal, handlers); } else if (constPlsPlaylist == getExtension(f.fileName())) { files += expandPlsPlaylist(f.absoluteFilePath(), useServer, useLocal, handlers); } else if (constXspfPlaylist == getExtension(f.fileName())) { files += expandXspfPlaylist(f.absoluteFilePath(), useServer, useLocal, handlers); } else if (checkExtension(f.fileName())) { QString fUrl=fileUrl(f.absoluteFilePath(), useServer, useLocal); if (!fUrl.isEmpty()) { files.append(fUrl); } } } } return files; } QStringList PlayQueueModel::parseUrls(const QStringList &urls) { QStringList useable; bool useServer = HttpServer::self()->isAlive(); bool useLocal = MPDConnection::self()->localFilePlaybackSupported(); QSet handlers = MPDConnection::self()->urlHandlers(); bool haveLocalFiles = false; for (const auto &path: urls) { QUrl u=path.indexOf("://")>2 ? QUrl(path) : QUrl::fromLocalFile(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()) { haveLocalFiles = true; QDir d(u.path()); if (d.exists()) { useable = listFiles(d, useServer, useLocal, handlers); } else if (checkExtension(u.path(), constM3uPlaylists)) { useable += expandM3uPlaylist(u.path(), useServer, useLocal, handlers); } else if (constPlsPlaylist == getExtension(u.path())) { useable += expandPlsPlaylist(u.path(), useServer, useLocal, handlers); } else if (constXspfPlaylist == getExtension(u.path())) { useable += expandXspfPlaylist(u.path(), useServer, useLocal, handlers); } else if (checkExtension(u.path())) { QString fUrl = fileUrl(u.path(), useServer, useLocal); if (!fUrl.isEmpty()) { useable.append(fUrl); } } } } QStringList use; if (haveLocalFiles && !useServer && !useLocal) { #ifdef ENABLE_HTTP_SERVER emit error(tr("Cannot add local files. Please enable in-built HTTP server, or configure MPD for local file playback.")); #else emit error(tr("Cannot add local files. Please configure MPD for local file playback.")); #endif return use; } // Ensure we only have unqiue URLs... QSet unique; for (const auto &u: useable) { if (!unique.contains(u)) { unique.insert(u); use.append(u); } } if (use.isEmpty()) { emit error(tr("Unable to add local files. No suitable files found.")); } return use; } void PlayQueueModel::encode(QMimeData &mimeData, const QString &mime, const QStringList &values) { QByteArray encodedData; QTextStream stream(&encodedData, QIODevice::WriteOnly); for (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); for (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 tr("Title"); case COL_ARTIST: return tr("Artist"); case COL_ALBUM: return tr("Album"); case COL_TRACK: return tr("#", "Track number"); case COL_LENGTH: return tr("Length"); case COL_DISC: return tr("Disc"); case COL_YEAR: return tr("Year"); case COL_ORIGYEAR: return tr("Original Year"); case COL_GENRE: return tr("Genre"); case COL_PRIO: return tr("Priority"); case COL_COMPOSER: return tr("Composer"); case COL_PERFORMER: return tr("Performer"); case COL_RATING: return tr("Rating"); case COL_FILENAME: return tr("Filename"); case COL_PATH: return tr("Path"); default: return QString(); } } PlayQueueModel::PlayQueueModel(QObject *parent) : QAbstractItemModel(parent) , currentSongId(-1) , currentSongRowNum(-1) , time(0) , mpdState(MPDState_Inactive) , stopAfterCurrent(false) , stopAfterTrackId(-1) , undoLimit(10) , undoEnabled(undoLimit>0) , lastCommand(Cmd_Other) , dropAdjust(0) { fetcher=new StreamFetcher(this); connect(this, SIGNAL(modelReset()), this, SLOT(stats())); connect(fetcher, SIGNAL(result(const QStringList &, int, int, quint8, bool)), SLOT(addFiles(const QStringList &, int, int, quint8, bool))); connect(fetcher, SIGNAL(result(const QStringList &, int, int, quint8, bool)), SIGNAL(streamsFetched())); connect(fetcher, SIGNAL(status(QString)), SIGNAL(streamFetchStatus(QString))); connect(this, SIGNAL(filesAdded(const QStringList, quint32, quint32, int, quint8, bool)), MPDConnection::self(), SLOT(add(const QStringList, quint32, quint32, int, quint8, bool))); 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(this, SIGNAL(setOrder(const QList &)), MPDConnection::self(), SLOT(setOrder(const QList &))); connect(MPDConnection::self(), SIGNAL(prioritySet(const QMap &)), SLOT(prioritySet(const QMap &))); 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))); connect(this, SIGNAL(getRating(QString)), MPDConnection::self(), SLOT(getRating(QString))); connect(this, SIGNAL(setRating(QStringList,quint8)), MPDConnection::self(), SLOT(setRating(QStringList,quint8))); connect(MPDConnection::self(), SIGNAL(rating(QString,quint8)), SLOT(ratingResult(QString,quint8))); connect(MPDConnection::self(), SIGNAL(stickerDbChanged()), SLOT(stickerDbChanged())); #ifdef ENABLE_DEVICES_SUPPORT //TODO: Problems here with devices support!!! connect(DevicesModel::self(), SIGNAL(updatedDetails(QList)), SLOT(updateDetails(QList))); #endif removeDuplicatesAction=new Action(tr("Remove Duplicates"), this); removeDuplicatesAction->setEnabled(false); QColor col=Utils::monoIconColor(); undoAction=ActionCollection::get()->createAction("playqueue-undo", tr("Undo"), MonoIcon::icon(FontAwesome::undo, col)); undoAction->setShortcut(Qt::ControlModifier+Qt::Key_Z); redoAction=ActionCollection::get()->createAction("playqueue-redo", tr("Redo"), MonoIcon::icon(FontAwesome::repeat, col)); redoAction->setShortcut(Qt::ControlModifier+Qt::ShiftModifier+Qt::Key_Z); connect(undoAction, SIGNAL(triggered()), this, SLOT(undo())); connect(redoAction, SIGNAL(triggered()), this, SLOT(redo())); connect(removeDuplicatesAction, SIGNAL(triggered()), this, SLOT(removeDuplicates())); shuffleAction=new Action(tr("Shuffle"), this); shuffleAction->setMenu(new QMenu(nullptr)); Action *shuffleTracksAction = new Action(tr("Tracks"), shuffleAction); Action *shuffleAlbumsAction = new Action(tr("Albums"), shuffleAction); connect(shuffleTracksAction, SIGNAL(triggered()), MPDConnection::self(), SLOT(shuffle())); connect(shuffleAlbumsAction, SIGNAL(triggered()), this, SLOT(shuffleAlbums())); shuffleAction->menu()->addAction(shuffleTracksAction); shuffleAction->menu()->addAction(shuffleAlbumsAction); sortAction=new Action(tr("Sort By"), this); sortAction->setMenu(new QMenu(nullptr)); addSortAction(tr("Artist"), constSortByArtistKey); addSortAction(tr("Album Artist"), constSortByAlbumArtistKey); addSortAction(tr("Album"), constSortByAlbumKey); addSortAction(tr("Track Title"), constSortByTitleKey); addSortAction(tr("Track Number"), constSortByNumberKey); addSortAction(tr("Genre"), constSortByGenreKey); addSortAction(tr("Year"), constSortByYearKey); addSortAction(tr("Composer"), constSortByComposerKey); addSortAction(tr("Performer"), constSortByPerformerKey); controlActions(); shuffleAction->setEnabled(false); sortAction->setEnabled(false); alignments[COL_TITLE]=alignments[COL_ARTIST]=alignments[COL_ALBUM]=alignments[COL_GENRE]=alignments[COL_COMPOSER]= alignments[COL_PERFORMER]=alignments[COL_FILENAME]=alignments[COL_PATH]=int(Qt::AlignVCenter|Qt::AlignLeft); alignments[COL_TRACK]=alignments[COL_LENGTH]=alignments[COL_DISC]=alignments[COL_YEAR]=alignments[COL_ORIGYEAR]= alignments[COL_PRIO]=int(Qt::AlignVCenter|Qt::AlignRight); alignments[COL_RATING]=int(Qt::AlignVCenter|Qt::AlignHCenter); } 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) { switch (role) { case Qt::DisplayRole: return headerText(section); case Qt::TextAlignmentRole: return alignments[section]; case Cantata::Role_InitiallyHidden: return COL_YEAR==section || COL_ORIGYEAR==section || COL_DISC==section || COL_GENRE==section || COL_PRIO==section || COL_COMPOSER==section || COL_PERFORMER==section || COL_RATING==section || COL_FILENAME==section || COL_PATH==section; case Cantata::Role_Hideable: return COL_LENGTH!=section; 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_ORIGYEAR: 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; case COL_RATING: return 0.08; case COL_FILENAME: return 0.27; case COL_PATH: return 0.27; } case Cantata::Role_ContextMenuText: return COL_TRACK==section ? tr("# (Track Number)") : headerText(section); default: break; } } return QVariant(); } bool PlayQueueModel::setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role) { if (Qt::Horizontal==orientation && Qt::TextAlignmentRole==role && section>=0) { int al=value.toInt()|Qt::AlignVCenter; if (al!=alignments[section]) { alignments[section]=al; return true; } } return false; } 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); } QVariant PlayQueueModel::data(const QModelIndex &index, int role) const { if (!index.isValid() && Cantata::Role_RatingCol==role) { return COL_RATING; } if (!index.isValid() || index.row() >= songs.size()) { return QVariant(); } 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+QString(" – ")+s.displayAlbum(); } case Cantata::Role_Time: { const Song &s=songs.at(index.row()); return s.time>0 ? Utils::formatTime(s.time) : QLatin1String(""); } 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_SongWithRating: case Cantata::Role_Song: { QVariant var; const Song &s=songs.at(index.row()); if (Cantata::Role_SongWithRating==role && Song::Standard==s.type && Song::Rating_Null==s.rating) { emit getRating(s.file); s.rating=Song::Rating_Requested; } var.setValue(s); 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; iinfoTooltips()) { return QVariant(); } Song s=songs.at(index.row()); if (s.album.isEmpty() && s.isStream()) { return basicPath(s); } else { return s.toolTip(); } } case Qt::TextAlignmentRole: return alignments[index.column()]; case Cantata::Role_Decoration: { qint32 id=songs.at(index.row()).id; if (id==currentSongId) { switch (mpdState) { case MPDState_Inactive: case MPDState_Stopped: return MonoIcon::icon(FontAwesome::stop, Utils::monoIconColor()); case MPDState_Playing: return MonoIcon::icon(stopAfterCurrent ? FontAwesome::playcircle : FontAwesome::play, Utils::monoIconColor()); case MPDState_Paused: return MonoIcon::icon(FontAwesome::pause, Utils::monoIconColor()); } } else if (-1!=id && id==stopAfterTrackId) { return MonoIcon::icon(FontAwesome::stop, Utils::monoIconColor()); } break; } default: break; } return QVariant(); } 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); } } 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 { 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; for (const 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, false); return true; } else if(data->hasFormat(constUriMimeType)/* && MPDConnection::self()->getDetails().isLocal()*/) { QStringList useable=parseUrls(decode(*data, constUriMimeType)); if (useable.count()) { addItems(useable, row, false, 0, false); return true; } } return false; } void PlayQueueModel::load(const QStringList &urls, int action, quint8 priority, bool decreasePriority) { if (-1==action) { action = songs.isEmpty() ? MPDConnection::AppendAndPlay : MPDConnection::Append; } QStringList useable=parseUrls(urls); if (useable.count()) { addItems(useable, songs.count(), action, priority, decreasePriority); } } void PlayQueueModel::addItems(const QStringList &items, int row, int action, quint8 priority, bool decreasePriority) { bool haveRadioStream=false; for (const QString &f: items) { QUrl u(f); if (u.scheme().startsWith(StreamsModel::constPrefix)) { haveRadioStream=true; break; } } if (haveRadioStream) { emit fetchingStreams(); fetcher->get(items, row, action, priority, decreasePriority); } else { addFiles(items, row, action, priority, decreasePriority); } } void PlayQueueModel::addFiles(const QStringList &filenames, int row, int action, quint8 priority, bool decreasePriority) { if (MPDConnection::ReplaceAndplay==action) { emit filesAdded(filenames, 0, 0, MPDConnection::ReplaceAndplay, priority, decreasePriority); } else if (songs.isEmpty()) { emit filesAdded(filenames, 0, 0, action, priority, decreasePriority); } else if (row < 0) { emit filesAdded(filenames, songs.size(), songs.size(), action, priority, decreasePriority); } else { emit filesAdded(filenames, row, songs.size(), action, priority, decreasePriority); } } void PlayQueueModel::prioritySet(const QMap &prio) { QList prev; if (undoEnabled) { for (const Song &s: songs) { prev.append(s); } } QMap copy = prio; int row=0; for (const Song &s: songs) { QMap::ConstIterator it = copy.find(s.id); if (copy.end()!=it) { s.priority=it.value(); copy.remove(s.id); QModelIndex idx(index(row, 0)); emit dataChanged(idx, idx); if (copy.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 { if (CueFile::isCue(file)) { QUrl u(file); QUrlQuery q(u); Song check; check.album=q.queryItemValue("album"); check.artist=q.queryItemValue("artist"); check.albumartist=q.queryItemValue("albumartist"); check.album=q.queryItemValue("album"); check.title=q.queryItemValue("title"); check.disc=q.queryItemValue("disc").toUInt(); check.track=q.queryItemValue("track").toUInt(); check.time=q.queryItemValue("time").toUInt(); check.year=q.queryItemValue("year").toUInt(); check.origYear=q.queryItemValue("origYear").toUInt(); for (const Song &s: songs) { if (s.sameMetadata(check)) { return s.id; } } } else { for (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 { for (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); if (currentSongRowNum>=0 && currentSongRowNum<=songs.count()) { const Song &song=songs.at(currentSongRowNum); if (Song::Rating_Null==song.rating) { song.rating=Song::Rating_Requested; emit getRating(song.file); } else if (Song::Rating_Requested!=song.rating) { emit currentSongRating(song.file, song.rating); } } 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, bool isComplete) { currentSongRowNum=-1; if (songList.isEmpty()) { Song::clearKeyStore(MPDParseUtils::Loc_PlayQueue); } removeDuplicatesAction->setEnabled(songList.count()>1); QList prev; if (undoEnabled) { prev=songs; } QSet newIds; for (const Song &s: songList) { newIds.insert(s.id); } // If we have too many changes UI can hang, so it is sometimes better just to do a complete reset! if (isComplete && (songs.count()>MPDConnection::constMaxPqChanges || !MPDConnection::self()->isPlayQueueIdValid())) { songs.clear(); } if (songs.isEmpty() || songList.isEmpty()) { beginResetModel(); songs=songList; ids=newIds; endResetModel(); if (songList.isEmpty()) { stopAfterTrackId=-1; } } else { time = 0; QSet removed=ids-newIds; for (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 currentSongAtPos=newSong ? Song() : songs.at(i); bool isEmpty=s.isEmpty(); if (newSong || s.id!=currentSongAtPos.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; s.rating=old.rating; songs.insert(i, isEmpty ? old : s); endMoveRows(); } } else if (isEmpty) { s=currentSongAtPos; } else { s.key=currentSongAtPos.key; s.rating=currentSongAtPos.rating; songs.replace(i, s); if (s.title!=currentSongAtPos.title || s.artist!=currentSongAtPos.artist || s.name()!=currentSongAtPos.name()) { emit dataChanged(index(i, 0), index(i, columnCount(QModelIndex())-1)); } } if (s.id==currentSongId) { currentSongRowNum=i; } 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; isetEnabled(songs.count()>1); sortAction->setEnabled(songs.count()>1); } 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(bool cdOnly) { QList ids; for (const Song &s: songs) { if (s.isCdda() || (!cdOnly && s.isCantataStream())) { 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; for (const int &r: rowsToRemove) { if (r>-1 && r &rowsToKeep) { QSet allIds; for (const Song &song: songs) { allIds.insert(song.id); } QSet keepIds; for (const int &r: rowsToKeep) { if (r>-1 && r removeIds=allIds-keepIds; if (!removeIds.isEmpty()) { emit removeSongs(removeIds.toList()); } } void PlayQueueModel::setRating(const QList &rows, quint8 rating) const { QSet files; for (const int &r: rows) { if (r>-1 && r0; if (!e) { undoStack.clear(); redoStack.clear(); } controlActions(); } static PlayQueueModel::UndoItem getState(const QList &songs) { PlayQueueModel::UndoItem item; for (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(); } } controlActions(); lastCommand=Cmd_Other; } 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()), SLOT(sortBy())); } static bool composerSort(const Song *s1, const Song *s2) { const QString v1=s1->hasComposer() ? s1->composer() : QString(); const QString v2=s2->hasComposer() ? s2->composer() : QString(); int c=v1.localeAwareCompare(v2); return c<0 || (c==0 && (*s1)<(*s2)); } static bool performerSort(const Song *s1, const Song *s2) { const QString v1=s1->hasPerformer() ? s1->performer() : QString(); const QString v2=s2->hasPerformer() ? s2->performer() : QString(); int c=v1.localeAwareCompare(v2); return c<0 || (c==0 && (*s1)<(*s2)); } static bool artistSort(const Song *s1, const Song *s2) { const QString v1=s1->hasArtistSort() ? s1->artistSort() : s1->artist; const QString v2=s2->hasArtistSort() ? s2->artistSort() : s2->artist; int c=v1.localeAwareCompare(v2); return c<0 || (c==0 && (*s1)<(*s2)); } static bool albumArtistSort(const Song *s1, const Song *s2) { const QString v1=s1->hasAlbumArtistSort() ? s1->albumArtistSort() : s1->albumArtistOrComposer(); const QString v2=s2->hasAlbumArtistSort() ? s2->albumArtistSort() : s2->albumArtistOrComposer(); int c=v1.localeAwareCompare(v2); return c<0 || (c==0 && (*s1)<(*s2)); } static bool albumSort(const Song *s1, const Song *s2) { const QString v1=s1->hasAlbumSort() ? s1->albumSort() : s1->album; const QString v2=s2->hasAlbumSort() ? s2->albumSort() : s2->album; int c=v1.localeAwareCompare(v2); return c<0 || (c==0 && (*s1)<(*s2)); } static bool genreSort(const Song *s1, const Song *s2) { int c=s1->compareGenres(*s2); return c<0 || (c==0 && (*s1)<(*s2)); } static bool yearSort(const Song *s1, const Song *s2) { return s1->yearyear || (s1->year==s2->year && (*s1)<(*s2)); } static bool titleSort(const Song *s1, const Song *s2) { int c=s1->title.localeAwareCompare(s2->title); return c<0 || (c==0 && (*s1)<(*s2)); } static bool trackSort(const Song *s1, const Song *s2) { return s1->tracktrack || (s1->track==s2->track && (*s1)<(*s2)); } void PlayQueueModel::sortBy() { Action *act=qobject_cast(sender()); if (act) { QString key=act->property(constSortByKey).toString(); QList copy; for (const Song &s: songs) { ((Song &)s).populateSorts(); copy.append(&s); } 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); } else if (constSortByComposerKey==key) { qSort(copy.begin(), copy.end(), composerSort); } else if (constSortByPerformerKey==key) { qSort(copy.begin(), copy.end(), performerSort); } else if (constSortByTitleKey==key) { qSort(copy.begin(), copy.end(), titleSort); } else if (constSortByNumberKey==key) { qSort(copy.begin(), copy.end(), trackSort); } QList positions; for (const Song *s: copy) { positions.append(getRowById(s->id)); } emit setOrder(positions); } } void PlayQueueModel::removeDuplicates() { QMap > map; for (const Song &song: songs) { map[song.albumKey()+"-"+song.artistSong()+"-"+song.track].append(song); } QList toRemove; for (const QString &key: map.keys()) { QList values=map.value(key); if (values.size()>1) { Song::sortViaType(values); for (int i=1; i::iterator it=songs.begin(); QList::iterator end=songs.end(); int numCols=columnCount(QModelIndex())-1; for (int row=0; it!=end; ++it, ++row) { if (Song::Standard==(*it).type && r!=(*it).rating && (*it).file==file) { (*it).rating=r; emit dataChanged(index(row, 0), index(row, numCols)); if ((*it).id==currentSongId) { emit currentSongRating(file, r); } } } } void PlayQueueModel::stickerDbChanged() { // Sticker DB changed, need to re-request ratings... QSet requests; for (const Song &song: songs) { if (Song::Standard==song.type && song.rating<=Song::Rating_Max && !requests.contains(song.file)) { emit getRating(song.file); requests.insert(song.file); } } } void PlayQueueModel::undo() { if (!undoEnabled || undoStack.isEmpty()) { return; } UndoItem item=undoStack.top(); emit populate(item.files, item.priority); lastCommand=Cmd_Undo; } void PlayQueueModel::redo() { if (!undoEnabled || redoStack.isEmpty()) { return; } UndoItem item=redoStack.top(); emit populate(item.files, item.priority); lastCommand=Cmd_Redo; } void PlayQueueModel::playSong(const QString &file) { qint32 id=getSongId(file); if (-1==id) { emit addAndPlay(file); } else { emit startPlayingSongId(id); } } void PlayQueueModel::stats() { time = 0; //Loop over all songs for (const Song &song: songs) { time += song.time; } emit statsUpdated(songs.size(), time); } void PlayQueueModel::cancelStreamFetch() { fetcher->cancel(); } static bool songSort(const Song *s1, const Song *s2) { return s1->discdisc || (s1->disc==s2->disc && (s1->tracktrack || (s1->track==s2->track && (*s1)<(*s2)))); } void PlayQueueModel::shuffleAlbums() { QMap > albums; for (const Song &s: songs) { albums[s.key].append(&s); } QList keys=albums.keys(); if (keys.count()<2) { return; } QList positions; while (!keys.isEmpty()) { quint32 key=keys.takeAt(Utils::random(keys.count())); QList albumSongs=albums[key]; qSort(albumSongs.begin(), albumSongs.end(), songSort); for (const Song *song: albumSongs) { positions.append(getRowById(song->id)); } } emit setOrder(positions); } 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; for (const Song &song: rem) { s.insert(song.file); } for (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; for (const Song &song: updated) { songMap[song.file]=song; } for (int i=0; i