1539 lines
48 KiB
C++
1539 lines
48 KiB
C++
/*
|
||
* Cantata
|
||
*
|
||
* Copyright (c) 2011-2018 Craig Drummond <craig.p.drummond@gmail.com>
|
||
*
|
||
*/
|
||
/*
|
||
* 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 <http://www.gnu.org/licenses/>.
|
||
*/
|
||
|
||
#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 <QPalette>
|
||
#include <QFont>
|
||
#include <QModelIndex>
|
||
#include <QMimeData>
|
||
#include <QTextStream>
|
||
#include <QSet>
|
||
#include <QUrl>
|
||
#include <QUrlQuery>
|
||
#include <QTimer>
|
||
#include <QApplication>
|
||
#include <QMenu>
|
||
|
||
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");
|
||
|
||
QSet<QString> PlayQueueModel::constFileExtensions = QSet<QString>()
|
||
<< 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");
|
||
|
||
static bool checkExtension(const QString &file)
|
||
{
|
||
int pos=file.lastIndexOf('.');
|
||
return pos>1 ? PlayQueueModel::constFileExtensions.contains(file.mid(pos+1).toLower()) : false;
|
||
}
|
||
|
||
static QStringList listFiles(const QDir &d, int level=5)
|
||
{
|
||
QStringList files;
|
||
if (level) {
|
||
for (const auto &f: d.entryInfoList(QDir::Files|QDir::Dirs|QDir::NoDotAndDotDot|QDir::NoSymLinks)) {
|
||
if (f.isDir()) {
|
||
files += listFiles(QDir(f.absoluteFilePath()), level-1);
|
||
} else if (checkExtension(f.fileName())) {
|
||
files.append(f.absoluteFilePath());
|
||
}
|
||
}
|
||
}
|
||
return files;
|
||
}
|
||
|
||
static QStringList parseUrls(const QStringList &urls, bool percentEncoded)
|
||
{
|
||
QStringList useable;
|
||
bool useServer = HttpServer::self()->isAlive();
|
||
for (const auto &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()) {
|
||
QStringList files;
|
||
QDir d(u.path());
|
||
|
||
if (d.exists()) {
|
||
files = listFiles(d);
|
||
} else if (checkExtension(u.path())) {
|
||
files.append(u.path());
|
||
}
|
||
|
||
for (const auto &file : files) {
|
||
if (useServer) {
|
||
QByteArray path = HttpServer::self()->encodeUrl(file);
|
||
if (!path.isEmpty()) {
|
||
useable.append(path);
|
||
}
|
||
} else if (MPDConnection::self()->localFilePlaybackSupported() && !file.startsWith(QLatin1String("/media/"))) {
|
||
useable.append(QLatin1String("file://")+file);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return useable;
|
||
}
|
||
|
||
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<quint32> &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<quint32> PlayQueueModel::decodeInts(const QMimeData &mimeData, const QString &mime)
|
||
{
|
||
QByteArray encodedData=mimeData.data(mime);
|
||
QDataStream stream(&encodedData, QIODevice::ReadOnly);
|
||
QList<quint32> 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<quint8>)), MPDConnection::self(), SLOT(populate(QStringList, QList<quint8>)));
|
||
connect(this, SIGNAL(move(const QList<quint32> &, quint32, quint32)),
|
||
MPDConnection::self(), SLOT(move(const QList<quint32> &, quint32, quint32)));
|
||
connect(this, SIGNAL(setOrder(const QList<quint32> &)), MPDConnection::self(), SLOT(setOrder(const QList<quint32> &)));
|
||
connect(MPDConnection::self(), SIGNAL(prioritySet(const QMap<qint32, quint8> &)), SLOT(prioritySet(const QMap<qint32, 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<qint32>)), MPDConnection::self(), SLOT(removeSongs(QList<qint32>)));
|
||
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<Song>)), SLOT(updateDetails(QList<Song>)));
|
||
#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<Song>(s);
|
||
return var;
|
||
}
|
||
case Cantata::Role_AlbumDuration: {
|
||
const Song &first = songs.at(index.row());
|
||
quint32 d=first.time;
|
||
for (int i=index.row()+1; i<songs.count(); ++i) {
|
||
const Song &song = songs.at(i);
|
||
if (song.key!=first.key) {
|
||
break;
|
||
}
|
||
d+=song.time;
|
||
}
|
||
if (index.row()>1) {
|
||
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; i<songs.count(); ++i) {
|
||
const Song &song = songs.at(i);
|
||
if (song.key!=first.key) {
|
||
break;
|
||
}
|
||
count++;
|
||
}
|
||
if (index.row()>1) {
|
||
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<songs.count(); ++i) {
|
||
const Song &s=songs.at(i);
|
||
if (s.key!=key) {
|
||
return QVariant();
|
||
}
|
||
if (s.id==currentSongId) {
|
||
switch (mpdState) {
|
||
case MPDState_Inactive:
|
||
case MPDState_Stopped: return (int)GroupedView::State_Stopped;
|
||
case MPDState_Playing: return (int)(stopAfterCurrent ? GroupedView::State_StopAfter : GroupedView::State_Playing);
|
||
case MPDState_Paused: return (int)GroupedView::State_Paused;
|
||
}
|
||
} else if (-1!=s.id && s.id==stopAfterTrackId) {
|
||
return GroupedView::State_StopAfterTrack;
|
||
}
|
||
}
|
||
return QVariant();
|
||
}
|
||
case Cantata::Role_Status: {
|
||
qint32 id=songs.at(index.row()).id;
|
||
if (id==currentSongId) {
|
||
switch (mpdState) {
|
||
case MPDState_Inactive:
|
||
case MPDState_Stopped: return (int)GroupedView::State_Stopped;
|
||
case MPDState_Playing: return (int)(stopAfterCurrent ? GroupedView::State_StopAfter : GroupedView::State_Playing);
|
||
case MPDState_Paused: return (int)GroupedView::State_Paused;
|
||
}
|
||
} else if (-1!=id && id==stopAfterTrackId) {
|
||
return GroupedView::State_StopAfterTrack;
|
||
}
|
||
return (int)GroupedView::State_Default;
|
||
break;
|
||
}
|
||
case Qt::FontRole: {
|
||
Song s=songs.at(index.row());
|
||
|
||
if (s.isStream()) {
|
||
QFont font;
|
||
if (songs.at(index.row()).id == currentSongId) {
|
||
font.setBold(true);
|
||
}
|
||
font.setItalic(true);
|
||
return font;
|
||
}
|
||
else if (songs.at(index.row()).id == currentSongId) {
|
||
QFont font;
|
||
font.setBold(true);
|
||
return font;
|
||
}
|
||
break;
|
||
}
|
||
// case Qt::BackgroundRole:
|
||
// if (songs.at(index.row()).id == currentSongId) {
|
||
// QColor col(QPalette().color(QPalette::Highlight));
|
||
// col.setAlphaF(0.2);
|
||
// return QVariant(col);
|
||
// }
|
||
// break;
|
||
case Qt::DisplayRole: {
|
||
const Song &song = songs.at(index.row());
|
||
switch (index.column()) {
|
||
case COL_TITLE:
|
||
return song.title.isEmpty() ? Utils::getFile(basicPath(song)) : song.title;
|
||
case COL_ARTIST:
|
||
return song.artist.isEmpty() ? Song::unknown() : song.artist;
|
||
case COL_ALBUM:
|
||
if (song.isStream() && song.album.isEmpty()) {
|
||
QString n=song.name();
|
||
if (!n.isEmpty()) {
|
||
return n;
|
||
}
|
||
}
|
||
return song.album;
|
||
case COL_TRACK:
|
||
if (song.track <= 0) {
|
||
return QVariant();
|
||
}
|
||
return song.track;
|
||
case COL_LENGTH:
|
||
return Utils::formatTime(song.time);
|
||
case COL_DISC:
|
||
if (song.disc <= 0) {
|
||
return QVariant();
|
||
}
|
||
return song.disc;
|
||
case COL_YEAR:
|
||
if (song.year <= 0) {
|
||
return QVariant();
|
||
}
|
||
return song.year;
|
||
case COL_ORIGYEAR:
|
||
if (song.origYear <= 0) {
|
||
return QVariant();
|
||
}
|
||
return song.origYear;
|
||
case COL_GENRE:
|
||
return song.displayGenre();
|
||
case COL_PRIO:
|
||
return song.priority;
|
||
case COL_COMPOSER:
|
||
return song.composer();
|
||
case COL_PERFORMER:
|
||
return song.performer();
|
||
// case COL_RATING:{
|
||
// QVariant var;
|
||
// const Song &s=songs.at(index.row());
|
||
// if (Song::Standard==s.type && Song::constNullRating==s.rating) {
|
||
// emit getRating(s.file);
|
||
// s.rating=Song::constRatingRequested;
|
||
// }
|
||
// var.setValue(s.rating);
|
||
// return var;
|
||
// }
|
||
case COL_FILENAME:
|
||
return Utils::getFile(QUrl(song.file).path());
|
||
case COL_PATH: {
|
||
QString dir=Utils::getDir(QUrl(song.file).path());
|
||
return dir.endsWith("/") ? dir.left(dir.length()-1) : dir;
|
||
}
|
||
default:
|
||
break;
|
||
}
|
||
break;
|
||
}
|
||
case Qt::ToolTipRole: {
|
||
if (!Settings::self()->infoTooltips()) {
|
||
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<quint32> positions;
|
||
QStringList filenames;
|
||
|
||
for (const QModelIndex &index: indexes) {
|
||
if (index.isValid() && 0==index.column()) {
|
||
positions.append(index.row());
|
||
filenames.append(static_cast<Song *>(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), true);
|
||
if (useable.count()) {
|
||
addItems(useable, row, false, 0, false);
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void PlayQueueModel::load(const QStringList &urls, int action)
|
||
{
|
||
QStringList useable=parseUrls(urls, false);
|
||
if (useable.count()) {
|
||
addItems(useable, songs.count(), action, 0, false);
|
||
}
|
||
}
|
||
|
||
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, MPDConnection::Append, 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<qint32, quint8> &prio)
|
||
{
|
||
QList<Song> prev;
|
||
if (undoEnabled) {
|
||
for (const Song &s: songs) {
|
||
prev.append(s);
|
||
}
|
||
}
|
||
QMap<qint32, quint8> copy = prio;
|
||
int row=0;
|
||
|
||
for (const Song &s: songs) {
|
||
QMap<qint32, quint8>::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<Song> &songList, bool isComplete)
|
||
{
|
||
currentSongRowNum=-1;
|
||
if (songList.isEmpty()) {
|
||
Song::clearKeyStore(MPDParseUtils::Loc_PlayQueue);
|
||
}
|
||
|
||
removeDuplicatesAction->setEnabled(songList.count()>1);
|
||
QList<Song> prev;
|
||
if (undoEnabled) {
|
||
prev=songs;
|
||
}
|
||
|
||
QSet<qint32> 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<qint32> 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<songList.count(); ++i) {
|
||
Song s=songList.at(i);
|
||
bool newSong=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; i<toBeRemoved; ++i) {
|
||
songs.takeLast();
|
||
}
|
||
endRemoveRows();
|
||
}
|
||
|
||
ids=newIds;
|
||
if (-1!=stopAfterTrackId && !ids.contains(stopAfterTrackId)) {
|
||
stopAfterTrackId=-1;
|
||
}
|
||
emit statsUpdated(songs.size(), time);
|
||
}
|
||
|
||
saveHistory(prev);
|
||
shuffleAction->setEnabled(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<qint32> 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<int> &rowsToRemove)
|
||
{
|
||
QList<qint32> removeIds;
|
||
for (const int &r: rowsToRemove) {
|
||
if (r>-1 && r<songs.count()) {
|
||
removeIds.append(songs.at(r).id);
|
||
}
|
||
}
|
||
|
||
if (!removeIds.isEmpty()) {
|
||
emit removeSongs(removeIds);
|
||
}
|
||
}
|
||
|
||
void PlayQueueModel::crop(const QList<int> &rowsToKeep)
|
||
{
|
||
QSet<qint32> allIds;
|
||
for (const Song &song: songs) {
|
||
allIds.insert(song.id);
|
||
}
|
||
|
||
QSet<qint32> keepIds;
|
||
for (const int &r: rowsToKeep) {
|
||
if (r>-1 && r<songs.count()) {
|
||
keepIds.insert(songs.at(r).id);
|
||
}
|
||
}
|
||
|
||
QSet<qint32> removeIds=allIds-keepIds;
|
||
if (!removeIds.isEmpty()) {
|
||
emit removeSongs(removeIds.toList());
|
||
}
|
||
}
|
||
|
||
void PlayQueueModel::setRating(const QList<int> &rows, quint8 rating) const
|
||
{
|
||
QSet<QString> files;
|
||
for (const int &r: rows) {
|
||
if (r>-1 && r<songs.count()) {
|
||
const Song &s=songs.at(r);
|
||
if (Song::Standard==s.type && !files.contains(s.file)) {
|
||
files.insert(s.file);
|
||
}
|
||
}
|
||
}
|
||
emit setRating(files.toList(), rating);
|
||
}
|
||
|
||
void PlayQueueModel::enableUndo(bool e)
|
||
{
|
||
if (e==undoEnabled) {
|
||
return;
|
||
}
|
||
undoEnabled=e && undoLimit>0;
|
||
if (!e) {
|
||
undoStack.clear();
|
||
redoStack.clear();
|
||
}
|
||
controlActions();
|
||
}
|
||
|
||
static PlayQueueModel::UndoItem getState(const QList<Song> &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<Song> &a, const QList<Song> &b)
|
||
{
|
||
if (a.count()!=b.count()) {
|
||
return false;
|
||
}
|
||
|
||
for (int i=0; i<a.count(); ++i) {
|
||
const Song &sa=a.at(i);
|
||
const Song &sb=b.at(i);
|
||
if (sa.priority!=sb.priority || sa.file!=sb.file) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
void PlayQueueModel::saveHistory(const QList<Song> &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->artistOrComposer();
|
||
const QString v2=s2->hasAlbumArtistSort() ? s2->albumArtistSort() : s2->artistOrComposer();
|
||
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->year<s2->year || (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->track<s2->track || (s1->track==s2->track && (*s1)<(*s2));
|
||
}
|
||
|
||
void PlayQueueModel::sortBy()
|
||
{
|
||
Action *act=qobject_cast<Action *>(sender());
|
||
if (act) {
|
||
QString key=act->property(constSortByKey).toString();
|
||
QList<const Song *> 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<quint32> positions;
|
||
for (const Song *s: copy) {
|
||
positions.append(getRowById(s->id));
|
||
}
|
||
emit setOrder(positions);
|
||
}
|
||
}
|
||
|
||
void PlayQueueModel::removeDuplicates()
|
||
{
|
||
QMap<QString, QList<Song> > map;
|
||
for (const Song &song: songs) {
|
||
map[song.albumKey()+"-"+song.artistSong()+"-"+song.track].append(song);
|
||
}
|
||
|
||
QList<qint32> toRemove;
|
||
for (const QString &key: map.keys()) {
|
||
QList<Song> values=map.value(key);
|
||
if (values.size()>1) {
|
||
Song::sortViaType(values);
|
||
for (int i=1; i<values.count(); ++i) {
|
||
toRemove.append(values.at(i).id);
|
||
}
|
||
}
|
||
}
|
||
if (!toRemove.isEmpty()) {
|
||
emit removeSongs(toRemove);
|
||
}
|
||
}
|
||
|
||
void PlayQueueModel::ratingResult(const QString &file, quint8 r)
|
||
{
|
||
QList<Song>::iterator it=songs.begin();
|
||
QList<Song>::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<QString> 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->disc<s2->disc || (s1->disc==s2->disc && (s1->track<s2->track || (s1->track==s2->track && (*s1)<(*s2))));
|
||
}
|
||
|
||
void PlayQueueModel::shuffleAlbums()
|
||
{
|
||
QMap<quint32, QList<const Song *> > albums;
|
||
for (const Song &s: songs) {
|
||
albums[s.key].append(&s);
|
||
}
|
||
|
||
QList<quint32> keys=albums.keys();
|
||
if (keys.count()<2) {
|
||
return;
|
||
}
|
||
|
||
QList<quint32> positions;
|
||
while (!keys.isEmpty()) {
|
||
quint32 key=keys.takeAt(Utils::random(keys.count()));
|
||
QList<const Song *> 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<Song> &rem)
|
||
{
|
||
QSet<QString> s;
|
||
QList<qint32> 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<Song> &updated)
|
||
{
|
||
QMap<QString, Song> songMap;
|
||
QList<int> updatedRows;
|
||
bool currentUpdated=false;
|
||
Song currentSong;
|
||
|
||
for (const Song &song: updated) {
|
||
songMap[song.file]=song;
|
||
}
|
||
|
||
for (int i=0; i<songs.count(); ++i) {
|
||
Song current=songs.at(i);
|
||
if (songMap.contains(current.file)) {
|
||
Song updatedSong=songMap[current.file];
|
||
updatedSong.id=current.id;
|
||
updatedSong.setKey(MPDParseUtils::Loc_PlayQueue);
|
||
|
||
if (updatedSong.title!=current.title || updatedSong.artist!=current.artist || updatedSong.name()!=current.name()) {
|
||
songs.replace(i, updatedSong);
|
||
updatedRows.append(i);
|
||
if (currentSongId==current.id) {
|
||
currentUpdated=true;
|
||
currentSong=updatedSong;
|
||
}
|
||
}
|
||
|
||
songMap.remove(current.file);
|
||
if (songMap.isEmpty()) {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!updatedRows.isEmpty()) {
|
||
if (updatedRows.count()==updated.count()) {
|
||
beginResetModel();
|
||
endResetModel();
|
||
} else {
|
||
for (int row: updatedRows) {
|
||
emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex())-1));
|
||
}
|
||
}
|
||
}
|
||
|
||
if (currentUpdated) {
|
||
emit updateCurrent(currentSong);
|
||
}
|
||
}
|
||
|
||
QStringList PlayQueueModel::filenames()
|
||
{
|
||
QStringList names;
|
||
for (const Song &song: songs) {
|
||
names << song.file;
|
||
}
|
||
return names;
|
||
}
|