1 /*
2  * Cantata
3  *
4  * Copyright (c) 2011-2020 Craig Drummond <craig.p.drummond@gmail.com>
5  *
6  */
7 /*
8  * Copyright (c) 2008 Sander Knopper (sander AT knopper DOT tk) and
9  *                    Roeland Douma (roeland AT rullzer DOT com)
10  *
11  * This file is part of QtMPC.
12  *
13  * QtMPC is free software: you can redistribute it and/or modify
14  * it under the terms of the GNU General Public License as published by
15  * the Free Software Foundation, either version 2 of the License, or
16  * (at your option) any later version.
17  *
18  * QtMPC is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  * GNU General Public License for more details.
22  *
23  * You should have received a copy of the GNU General Public License
24  * along with QtMPC.  If not, see <http://www.gnu.org/licenses/>.
25  */
26 
27 #include "playqueuemodel.h"
28 #include "mpd-interface/mpdparseutils.h"
29 #include "mpd-interface/mpdstats.h"
30 #include "mpd-interface/cuefile.h"
31 #include "streams/streamfetcher.h"
32 #include "streamsmodel.h"
33 #include "http/httpserver.h"
34 #include "gui/settings.h"
35 #include "support/icon.h"
36 #include "support/monoicon.h"
37 #include "support/utils.h"
38 #include "config.h"
39 #include "support/action.h"
40 #include "support/actioncollection.h"
41 #include "support/globalstatic.h"
42 #include "gui/covers.h"
43 #include "widgets/groupedview.h"
44 #include "widgets/icons.h"
45 #include "roles.h"
46 #ifdef ENABLE_DEVICES_SUPPORT
47 #include "devicesmodel.h"
48 #if defined CDDB_FOUND || defined MUSICBRAINZ5_FOUND
49 #include "devices/audiocddevice.h"
50 #endif
51 #endif
52 #include <QPalette>
53 #include <QFont>
54 #include <QFile>
55 #include <QDir>
56 #include <QModelIndex>
57 #include <QMimeData>
58 #include <QTextStream>
59 #include <QSet>
60 #include <QUrl>
61 #include <QUrlQuery>
62 #include <QTimer>
63 #include <QApplication>
64 #include <QMenu>
65 #include <QXmlStreamReader>
66 #include <QRandomGenerator>
67 #include <algorithm>
68 
69 GLOBAL_STATIC(PlayQueueModel, instance)
70 
71 const QLatin1String PlayQueueModel::constMoveMimeType("cantata/move");
72 const QLatin1String PlayQueueModel::constFileNameMimeType("cantata/filename");
73 const QLatin1String PlayQueueModel::constUriMimeType("text/uri-list");
74 
75 static const char * constSortByKey="sort-by";
76 static const QLatin1String constSortByArtistKey("artist");
77 static const QLatin1String constSortByAlbumArtistKey("albumartist");
78 static const QLatin1String constSortByAlbumKey("album");
79 static const QLatin1String constSortByGenreKey("genre");
80 static const QLatin1String constSortByYearKey("year");
81 static const QLatin1String constSortByComposerKey("composer");
82 static const QLatin1String constSortByPerformerKey("performer");
83 static const QLatin1String constSortByTitleKey("title");
84 static const QLatin1String constSortByNumberKey("track");
85 
86 static QSet<QString> constM3uPlaylists = QSet<QString>() << QLatin1String("m3u") << QLatin1String("m3u8");
87 static const QString constPlsPlaylist = QLatin1String("pls");
88 static const QString constXspfPlaylist = QLatin1String("xspf");
89 QSet<QString> PlayQueueModel::constFileExtensions = QSet<QString>()
90                                                   << QLatin1String("mp3") << QLatin1String("ogg") << QLatin1String("flac") << QLatin1String("wma") << QLatin1String("m4a")
91                                                   << QLatin1String("m4b") << QLatin1String("mp4") << QLatin1String("m4p") << QLatin1String("wav") << QLatin1String("wv")
92                                                   << QLatin1String("wvp") << QLatin1String("aiff") << QLatin1String("aif") << QLatin1String("aifc") << QLatin1String("ape")
93                                                   << QLatin1String("spx") << QLatin1String("tta") << QLatin1String("mpc") << QLatin1String("mpp") << QLatin1String("mp+")
94                                                   << QLatin1String("dff") << QLatin1String("dsf") << QLatin1String("opus")
95                                                   // And playlists...
96                                                   << QLatin1String("m3u") << QLatin1String("m3u8") << constPlsPlaylist << constXspfPlaylist;
97 
getExtension(const QString & file)98 static inline QString getExtension(const QString &file)
99 {
100     int pos=file.lastIndexOf('.');
101     return pos>0 ? file.mid(pos+1).toLower() : QString();
102 }
103 
checkExtension(const QString & file,const QSet<QString> & exts=PlayQueueModel::constFileExtensions)104 static inline bool checkExtension(const QString &file, const QSet<QString> &exts = PlayQueueModel::constFileExtensions)
105 {
106     return exts.contains(getExtension(file));
107 }
108 
fileUrl(const QString & file,bool useServer,bool useLocal)109 static QString fileUrl(const QString &file, bool useServer, bool useLocal)
110 {
111     if (useServer) {
112         QByteArray path = HttpServer::self()->encodeUrl(file);
113         if (!path.isEmpty()) {
114             return path;
115         }
116     } else if (useLocal && !file.startsWith(QLatin1String("/media/"))) {
117         return QLatin1String("file://")+file;
118     }
119     return QString();
120 }
121 
checkUrl(const QString & url,const QDir & dir,bool useServer,bool useLocal,const QSet<QString> & handlers)122 static QString checkUrl(const QString &url, const QDir &dir, bool useServer, bool useLocal, const QSet<QString> &handlers)
123 {
124     int pos = url.indexOf(QLatin1String("://"));
125     QString handler = pos>0 ? url.left(pos+3).toLower() : QString();
126     if (!handler.isEmpty() && (QLatin1String("http://")==handler || QLatin1String("https://")==handler)) {
127         // Radio stream?
128         return StreamsModel::constPrefix+url;
129     } else if (handlers.contains(handler)) {
130         return url;
131     } else if (checkExtension(url)) {
132         QString path = dir.filePath(url);
133         if (QFile::exists(path)) { // Relative
134             return fileUrl(path, useServer, useLocal);
135         } else if (QFile::exists(url)) { // Absolute
136             return fileUrl(url, useServer, useLocal);
137         }
138     }
139     return QString();
140 }
141 
expandM3uPlaylist(const QString & playlist,bool useServer,bool useLocal,const QSet<QString> & handlers)142 static QStringList expandM3uPlaylist(const QString &playlist, bool useServer, bool useLocal, const QSet<QString> &handlers)
143 {
144     QStringList files;
145     QFile f(playlist);
146     QDir dir(Utils::getDir(playlist));
147 
148     if (f.open(QIODevice::ReadOnly|QIODevice::Text)) {
149         QTextStream in(&f);
150         while (!in.atEnd()) {
151             QString line = in.readLine();
152             if (!line.startsWith(QLatin1Char('#'))) {
153                 QString url = checkUrl(line, dir, useServer, useLocal, handlers);
154                 if (!url.isEmpty()) {
155                     files.append(url);
156                 }
157             }
158         }
159         f.close();
160     }
161     return files;
162 }
163 
expandPlsPlaylist(const QString & playlist,bool useServer,bool useLocal,const QSet<QString> & handlers)164 static QStringList expandPlsPlaylist(const QString &playlist, bool useServer, bool useLocal, const QSet<QString> &handlers)
165 {
166     QStringList files;
167     QFile f(playlist);
168     QDir dir(Utils::getDir(playlist));
169     QMap<unsigned int, QString> titles;
170     QMap<unsigned int, QString> urls;
171 
172     if (f.open(QIODevice::ReadOnly|QIODevice::Text)) {
173         QTextStream in(&f);
174         while (!in.atEnd()) {
175             QString line = in.readLine();
176             if (line.startsWith(QLatin1String("File"))) {
177                 QStringList parts=line.split("=");
178                 if (2==parts.length()) {
179                     QString path=parts[1].trimmed();
180                     unsigned int num=parts[0].left(4).toUInt();
181                     QString url=checkUrl(path, dir, useServer, useLocal, handlers);
182                     if (!url.isEmpty()) {
183                         urls.insert(num, url);
184                     }
185                 }
186             } else if (line.startsWith(QLatin1String("Title"))) {
187                 QStringList parts=line.split("=");
188                 if (2==parts.length()) {
189                     titles.insert(parts[0].left(5).toUInt(), parts[1].trimmed());
190                 }
191             }
192         }
193         f.close();
194     }
195 
196     auto it = urls.constBegin();
197     auto end = urls.constEnd();
198     for (; it!=end; ++it) {
199         if (titles.contains(it.key()) && (it.value().startsWith(QLatin1String("http://")) || it.value().startsWith("https://"))) {
200             files.append(it.value()+"#"+titles[it.key()]);
201         } else {
202             files.append(it.value());
203         }
204     }
205     return files;
206 }
207 
expandXspfPlaylist(const QString & playlist,bool useServer,bool useLocal,const QSet<QString> & handlers)208 static QStringList expandXspfPlaylist(const QString &playlist, bool useServer, bool useLocal, const QSet<QString> &handlers)
209 {
210     QStringList files;
211     QFile f(playlist);
212 
213     if (f.open(QIODevice::ReadOnly|QIODevice::Text)) {
214         QXmlStreamReader reader(&f);
215         QDir dir(Utils::getDir(playlist));
216 
217         while (!reader.atEnd()) {
218             reader.readNext();
219             if (QXmlStreamReader::StartElement==reader.tokenType() && QLatin1String("location")==reader.name()) {
220                 QString url=checkUrl(reader.readElementText().trimmed(), dir, useServer, useLocal, handlers);
221                 if (!url.isEmpty()) {
222                     files.append(url);
223                 }
224             }
225         }
226         f.close();
227     }
228 
229     return files;
230 }
231 
listFiles(const QDir & d,bool useServer,bool useLocal,const QSet<QString> & handlers,int level=5)232 static QStringList listFiles(const QDir &d, bool useServer, bool useLocal, const QSet<QString> &handlers, int level=5)
233 {
234     QStringList files;
235     if (level) {
236         for (const auto &f: d.entryInfoList(QDir::Files|QDir::Dirs|QDir::NoDotAndDotDot|QDir::NoSymLinks, QDir::LocaleAware|QDir::IgnoreCase)) {
237             if (f.isDir()) {
238                 files += listFiles(QDir(f.absoluteFilePath()), useServer, useLocal, handlers, level-1);
239             } else if (checkExtension(f.fileName(), constM3uPlaylists)) {
240                 files += expandM3uPlaylist(f.absoluteFilePath(), useServer, useLocal, handlers);
241             } else if (constPlsPlaylist == getExtension(f.fileName())) {
242                 files += expandPlsPlaylist(f.absoluteFilePath(), useServer, useLocal, handlers);
243             } else if (constXspfPlaylist == getExtension(f.fileName())) {
244                 files += expandXspfPlaylist(f.absoluteFilePath(), useServer, useLocal, handlers);
245             } else if (checkExtension(f.fileName())) {
246                 QString fUrl=fileUrl(f.absoluteFilePath(), useServer, useLocal);
247                 if (!fUrl.isEmpty()) {
248                     files.append(fUrl);
249                 }
250             }
251         }
252     }
253     return files;
254 }
255 
parseUrls(const QStringList & urls)256 QStringList PlayQueueModel::parseUrls(const QStringList &urls)
257 {
258     QStringList useable;
259     bool useServer = HttpServer::self()->isAlive();
260     bool useLocal = MPDConnection::self()->localFilePlaybackSupported();
261     QSet<QString> handlers = MPDConnection::self()->urlHandlers();
262     bool haveLocalFiles = false;
263 
264     for (const auto &path: urls) {
265         QUrl u=path.indexOf("://")>2 ? QUrl(path) : QUrl::fromLocalFile(path);
266         #if defined ENABLE_DEVICES_SUPPORT && (defined CDDB_FOUND || defined MUSICBRAINZ5_FOUND)
267         QString cdDevice=AudioCdDevice::getDevice(u);
268         if (!cdDevice.isEmpty()) {
269             DevicesModel::self()->playCd(cdDevice);
270         } else
271         #endif
272         if (QLatin1String("http")==u.scheme()) {
273             useable.append(u.toString());
274         } else if (u.scheme().isEmpty() || QLatin1String("file")==u.scheme()) {
275             haveLocalFiles = true;
276             QDir d(u.path());
277 
278             if (d.exists()) {
279                 useable = listFiles(d, useServer, useLocal, handlers);
280             } else if (checkExtension(u.path(), constM3uPlaylists)) {
281                 useable += expandM3uPlaylist(u.path(), useServer, useLocal, handlers);
282             } else if (constPlsPlaylist == getExtension(u.path())) {
283                 useable += expandPlsPlaylist(u.path(), useServer, useLocal, handlers);
284             } else if (constXspfPlaylist == getExtension(u.path())) {
285                 useable += expandXspfPlaylist(u.path(), useServer, useLocal, handlers);
286             } else if (checkExtension(u.path())) {
287                 QString fUrl = fileUrl(u.path(), useServer, useLocal);
288                 if (!fUrl.isEmpty()) {
289                     useable.append(fUrl);
290                 }
291             }
292         }
293     }
294 
295     QStringList use;
296 
297     if (haveLocalFiles && !useServer && !useLocal) {
298         #ifdef ENABLE_HTTP_SERVER
299         emit error(tr("Cannot add local files. Please enable in-built HTTP server, or configure MPD for local file playback."));
300         #else
301         emit error(tr("Cannot add local files. Please configure MPD for local file playback."));
302         #endif
303         return use;
304     }
305 
306     // Ensure we only have unqiue URLs...
307     QSet<QString> unique;
308     for (const auto &u: useable) {
309         if (!unique.contains(u)) {
310             unique.insert(u);
311             use.append(u);
312         }
313     }
314 
315     if (use.isEmpty()) {
316         emit error(tr("Unable to add local files. No suitable files found."));
317     }
318     return use;
319 }
320 
encode(QMimeData & mimeData,const QString & mime,const QStringList & values)321 void PlayQueueModel::encode(QMimeData &mimeData, const QString &mime, const QStringList &values)
322 {
323     QByteArray encodedData;
324     QTextStream stream(&encodedData, QIODevice::WriteOnly);
325 
326     for (const QString &v: values) {
327         stream << v << endl;
328     }
329 
330     mimeData.setData(mime, encodedData);
331 }
332 
encode(QMimeData & mimeData,const QString & mime,const QList<quint32> & values)333 void PlayQueueModel::encode(QMimeData &mimeData, const QString &mime, const QList<quint32> &values)
334 {
335     QByteArray encodedData;
336     QDataStream stream(&encodedData, QIODevice::WriteOnly);
337     for (quint32 v: values) {
338         stream << v;
339     }
340     mimeData.setData(mime, encodedData);
341 }
342 
decode(const QMimeData & mimeData,const QString & mime)343 QStringList PlayQueueModel::decode(const QMimeData &mimeData, const QString &mime)
344 {
345     QByteArray encodedData=mimeData.data(mime);
346     QTextStream stream(&encodedData, QIODevice::ReadOnly);
347     QStringList rv;
348 
349     while (!stream.atEnd()) {
350         rv.append(stream.readLine().remove('\n'));
351     }
352     return rv;
353 }
354 
decodeInts(const QMimeData & mimeData,const QString & mime)355 QList<quint32> PlayQueueModel::decodeInts(const QMimeData &mimeData, const QString &mime)
356 {
357     QByteArray encodedData=mimeData.data(mime);
358     QDataStream stream(&encodedData, QIODevice::ReadOnly);
359     QList<quint32> rv;
360 
361     while (!stream.atEnd()) {
362         quint32 v;
363         stream >> v;
364         rv.append(v);
365     }
366     return rv;
367 }
368 
headerText(int col)369 QString PlayQueueModel::headerText(int col)
370 {
371     switch (col) {
372     case COL_TITLE:     return tr("Title");
373     case COL_ARTIST:    return tr("Artist");
374     case COL_ALBUM:     return tr("Album");
375     case COL_TRACK:     return tr("#", "Track number");
376     case COL_LENGTH:    return tr("Length");
377     case COL_DISC:      return tr("Disc");
378     case COL_YEAR:      return tr("Year");
379     case COL_ORIGYEAR:  return tr("Original Year");
380     case COL_GENRE:     return tr("Genre");
381     case COL_PRIO:      return tr("Priority");
382     case COL_COMPOSER:  return tr("Composer");
383     case COL_PERFORMER: return tr("Performer");
384     case COL_RATING:    return tr("Rating");
385     case COL_FILENAME:  return tr("Filename");
386     case COL_PATH:      return tr("Path");
387     default:            return QString();
388     }
389 }
390 
PlayQueueModel(QObject * parent)391 PlayQueueModel::PlayQueueModel(QObject *parent)
392     : QAbstractItemModel(parent)
393     , currentSongId(-1)
394     , currentSongRowNum(-1)
395     , time(0)
396     , mpdState(MPDState_Inactive)
397     , stopAfterCurrent(false)
398     , stopAfterTrackId(-1)
399     , undoLimit(10)
400     , undoEnabled(undoLimit>0)
401     , lastCommand(Cmd_Other)
402     , dropAdjust(0)
403 {
404     fetcher=new StreamFetcher(this);
405     connect(this, SIGNAL(modelReset()), this, SLOT(stats()));
406     connect(fetcher, SIGNAL(result(const QStringList &, int, int, quint8, bool)), SLOT(addFiles(const QStringList &, int, int, quint8, bool)));
407     connect(fetcher, SIGNAL(result(const QStringList &, int, int, quint8, bool)), SIGNAL(streamsFetched()));
408     connect(fetcher, SIGNAL(status(QString)), SIGNAL(streamFetchStatus(QString)));
409     connect(this, SIGNAL(filesAdded(const QStringList, quint32, quint32, int, quint8, bool)),
410             MPDConnection::self(), SLOT(add(const QStringList, quint32, quint32, int, quint8, bool)));
411     connect(this, SIGNAL(populate(QStringList, QList<quint8>)), MPDConnection::self(), SLOT(populate(QStringList, QList<quint8>)));
412     connect(this, SIGNAL(move(const QList<quint32> &, quint32, quint32)),
413             MPDConnection::self(), SLOT(move(const QList<quint32> &, quint32, quint32)));
414     connect(this, SIGNAL(setOrder(const QList<quint32> &)), MPDConnection::self(), SLOT(setOrder(const QList<quint32> &)));
415     connect(MPDConnection::self(), SIGNAL(prioritySet(const QMap<qint32, quint8> &)), SLOT(prioritySet(const QMap<qint32, quint8> &)));
416     connect(MPDConnection::self(), SIGNAL(stopAfterCurrentChanged(bool)), SLOT(stopAfterCurrentChanged(bool)));
417     connect(this, SIGNAL(stop(bool)), MPDConnection::self(), SLOT(stopPlaying(bool)));
418     connect(this, SIGNAL(clearStopAfter()), MPDConnection::self(), SLOT(clearStopAfter()));
419     connect(this, SIGNAL(removeSongs(QList<qint32>)), MPDConnection::self(), SLOT(removeSongs(QList<qint32>)));
420     connect(this, SIGNAL(clearEntries()), MPDConnection::self(), SLOT(clear()));
421     connect(this, SIGNAL(addAndPlay(QString)), MPDConnection::self(), SLOT(addAndPlay(QString)));
422     connect(this, SIGNAL(startPlayingSongId(qint32)), MPDConnection::self(), SLOT(startPlayingSongId(qint32)));
423     connect(this, SIGNAL(getRating(QString)), MPDConnection::self(), SLOT(getRating(QString)));
424     connect(this, SIGNAL(setRating(QStringList,quint8)), MPDConnection::self(), SLOT(setRating(QStringList,quint8)));
425     connect(MPDConnection::self(), SIGNAL(rating(QString,quint8)), SLOT(ratingResult(QString,quint8)));
426     connect(MPDConnection::self(), SIGNAL(stickerDbChanged()), SLOT(stickerDbChanged()));
427     #ifdef ENABLE_DEVICES_SUPPORT //TODO: Problems here with devices support!!!
428     connect(DevicesModel::self(), SIGNAL(updatedDetails(QList<Song>)), SLOT(updateDetails(QList<Song>)));
429     #endif
430 
431     removeDuplicatesAction=new Action(tr("Remove Duplicates"), this);
432     removeDuplicatesAction->setEnabled(false);
433     QColor col=Utils::monoIconColor();
434     undoAction=ActionCollection::get()->createAction("playqueue-undo", tr("Undo"), MonoIcon::icon(FontAwesome::undo, col));
435     undoAction->setShortcut(Qt::ControlModifier+Qt::Key_Z);
436     redoAction=ActionCollection::get()->createAction("playqueue-redo", tr("Redo"), MonoIcon::icon(FontAwesome::repeat, col));
437     redoAction->setShortcut(Qt::ControlModifier+Qt::ShiftModifier+Qt::Key_Z);
438     connect(undoAction, SIGNAL(triggered()), this, SLOT(undo()));
439     connect(redoAction, SIGNAL(triggered()), this, SLOT(redo()));
440     connect(removeDuplicatesAction, SIGNAL(triggered()), this, SLOT(removeDuplicates()));
441 
442     shuffleAction=new Action(tr("Shuffle"), this);
443     shuffleAction->setMenu(new QMenu(nullptr));
444     Action *shuffleTracksAction = new Action(tr("Tracks"), shuffleAction);
445     Action *shuffleAlbumsAction = new Action(tr("Albums"), shuffleAction);
446     connect(shuffleTracksAction, SIGNAL(triggered()), MPDConnection::self(), SLOT(shuffle()));
447     connect(shuffleAlbumsAction, SIGNAL(triggered()), this, SLOT(shuffleAlbums()));
448     shuffleAction->menu()->addAction(shuffleTracksAction);
449     shuffleAction->menu()->addAction(shuffleAlbumsAction);
450 
451     sortAction=new Action(tr("Sort By"), this);
452     sortAction->setMenu(new QMenu(nullptr));
453     addSortAction(tr("Artist"), constSortByArtistKey);
454     addSortAction(tr("Album Artist"), constSortByAlbumArtistKey);
455     addSortAction(tr("Album"), constSortByAlbumKey);
456     addSortAction(tr("Track Title"), constSortByTitleKey);
457     addSortAction(tr("Track Number"), constSortByNumberKey);
458     addSortAction(tr("Genre"), constSortByGenreKey);
459     addSortAction(tr("Year"), constSortByYearKey);
460     addSortAction(tr("Composer"), constSortByComposerKey);
461     addSortAction(tr("Performer"), constSortByPerformerKey);
462     controlActions();
463     shuffleAction->setEnabled(false);
464     sortAction->setEnabled(false);
465     alignments[COL_TITLE]=alignments[COL_ARTIST]=alignments[COL_ALBUM]=alignments[COL_GENRE]=alignments[COL_COMPOSER]=
466             alignments[COL_PERFORMER]=alignments[COL_FILENAME]=alignments[COL_PATH]=int(Qt::AlignVCenter|Qt::AlignLeft);
467     alignments[COL_TRACK]=alignments[COL_LENGTH]=alignments[COL_DISC]=alignments[COL_YEAR]=alignments[COL_ORIGYEAR]=
468             alignments[COL_PRIO]=int(Qt::AlignVCenter|Qt::AlignRight);
469     alignments[COL_RATING]=int(Qt::AlignVCenter|Qt::AlignHCenter);
470 }
471 
~PlayQueueModel()472 PlayQueueModel::~PlayQueueModel()
473 {
474 }
475 
index(int row,int column,const QModelIndex & parent) const476 QModelIndex PlayQueueModel::index(int row, int column, const QModelIndex &parent) const
477 {
478     return hasIndex(row, column, parent) ? createIndex(row, column, (void *)&songs.at(row)) : QModelIndex();
479 }
480 
parent(const QModelIndex & idx) const481 QModelIndex PlayQueueModel::parent(const QModelIndex &idx) const
482 {
483     Q_UNUSED(idx)
484     return QModelIndex();
485 }
486 
headerData(int section,Qt::Orientation orientation,int role) const487 QVariant PlayQueueModel::headerData(int section, Qt::Orientation orientation, int role) const
488 {
489     if (Qt::Horizontal==orientation) {
490         switch (role) {
491         case Qt::DisplayRole:
492             return headerText(section);
493         case Qt::TextAlignmentRole:
494             return alignments[section];
495         case Cantata::Role_InitiallyHidden:
496             return COL_YEAR==section || COL_ORIGYEAR==section || COL_DISC==section || COL_GENRE==section || COL_PRIO==section ||
497                    COL_COMPOSER==section || COL_PERFORMER==section || COL_RATING==section || COL_FILENAME==section || COL_PATH==section;
498         case Cantata::Role_Hideable:
499             return COL_LENGTH!=section;
500         case Cantata::Role_Width:
501             switch (section) {
502             case COL_TRACK:     return 0.075;
503             case COL_DISC:      return 0.03;
504             case COL_TITLE:     return 0.3;
505             case COL_ARTIST:    return 0.27;
506             case COL_ALBUM:     return 0.27;
507             case COL_LENGTH:    return 0.05;
508             case COL_YEAR:      return 0.05;
509             case COL_ORIGYEAR:  return 0.05;
510             case COL_GENRE:     return 0.1;
511             case COL_PRIO:      return 0.015;
512             case COL_COMPOSER:  return 0.2;
513             case COL_PERFORMER: return 0.2;
514             case COL_RATING:    return 0.08;
515             case COL_FILENAME:  return 0.27;
516             case COL_PATH:      return 0.27;
517             }
518         case Cantata::Role_ContextMenuText:
519             return COL_TRACK==section ? tr("# (Track Number)") : headerText(section);
520         default:
521             break;
522         }
523     }
524 
525     return QVariant();
526 }
527 
setHeaderData(int section,Qt::Orientation orientation,const QVariant & value,int role)528 bool PlayQueueModel::setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role)
529 {
530     if (Qt::Horizontal==orientation && Qt::TextAlignmentRole==role && section>=0) {
531         int al=value.toInt()|Qt::AlignVCenter;
532         if (al!=alignments[section]) {
533             alignments[section]=al;
534             return true;
535         }
536     }
537     return false;
538 }
539 
rowCount(const QModelIndex & idx) const540 int PlayQueueModel::rowCount(const QModelIndex &idx) const
541 {
542     return idx.isValid() ? 0 : songs.size();
543 }
544 
basicPath(const Song & song)545 static QString basicPath(const Song &song)
546 {
547     #ifdef ENABLE_HTTP_SERVER
548     if (song.isCantataStream()) {
549         Song mod=HttpServer::self()->decodeUrl(song.file);
550         if (!mod.file.isEmpty()) {
551             return mod.file;
552         }
553     }
554     #endif
555     QString path=song.filePath();
556     int marker=path.indexOf(QLatin1Char('#'));
557     return -1==marker ? path : path.left(marker);
558 }
559 
data(const QModelIndex & index,int role) const560 QVariant PlayQueueModel::data(const QModelIndex &index, int role) const
561 {
562     if (!index.isValid() && Cantata::Role_RatingCol==role) {
563         return COL_RATING;
564     }
565 
566     if (!index.isValid() || index.row() >= songs.size()) {
567         return QVariant();
568     }
569 
570     switch (role) {
571     case Cantata::Role_MainText: {
572         const Song &s=songs.at(index.row());
573         return s.title.isEmpty() ? s.file : s.trackAndTitleStr(false);
574     }
575     case Cantata::Role_SubText: {
576         const Song &s=songs.at(index.row());
577         return s.artist+Song::constSep+s.displayAlbum();
578     }
579     case Cantata::Role_Time: {
580         const Song &s=songs.at(index.row());
581         return s.time>0 ? Utils::formatTime(s.time) : QLatin1String("");
582     }
583     case Cantata::Role_IsCollection:
584         return false;
585     case Cantata::Role_CollectionId:
586         return 0;
587     case Cantata::Role_Key:
588         return songs.at(index.row()).key;
589     case Cantata::Role_Id:
590         return songs.at(index.row()).id;
591     case Cantata::Role_SongWithRating:
592     case Cantata::Role_Song: {
593         QVariant var;
594         const Song &s=songs.at(index.row());
595         if (Cantata::Role_SongWithRating==role && Song::Standard==s.type && Song::Rating_Null==s.rating) {
596             emit getRating(s.file);
597             s.rating=Song::Rating_Requested;
598         }
599         var.setValue<Song>(s);
600         return var;
601     }
602     case Cantata::Role_AlbumDuration: {
603         const Song &first = songs.at(index.row());
604         quint32 d=first.time;
605         for (int i=index.row()+1; i<songs.count(); ++i) {
606             const Song &song = songs.at(i);
607             if (song.key!=first.key) {
608                 break;
609             }
610             d+=song.time;
611         }
612         if (index.row()>1) {
613             for (int i=index.row()-1; i<=0; ++i) {
614                 const Song &song = songs.at(i);
615                 if (song.key!=first.key) {
616                     break;
617                 }
618                 d+=song.time;
619             }
620         }
621         return d;
622     }
623     case Cantata::Role_SongCount: {
624         const Song &first = songs.at(index.row());
625         quint32 count=1;
626         for (int i=index.row()+1; i<songs.count(); ++i) {
627             const Song &song = songs.at(i);
628             if (song.key!=first.key) {
629                 break;
630             }
631             count++;
632         }
633         if (index.row()>1) {
634             for (int i=index.row()-1; i<=0; ++i) {
635                 const Song &song = songs.at(i);
636                 if (song.key!=first.key) {
637                     break;
638                 }
639                 count++;
640             }
641         }
642         return count;
643     }
644     case Cantata::Role_CurrentStatus: {
645         quint16 key=songs.at(index.row()).key;
646         for (int i=index.row()+1; i<songs.count(); ++i) {
647             const Song &s=songs.at(i);
648             if (s.key!=key) {
649                 return QVariant();
650             }
651             if (s.id==currentSongId) {
652                 switch (mpdState) {
653                 case MPDState_Inactive:
654                 case MPDState_Stopped: return (int)GroupedView::State_Stopped;
655                 case MPDState_Playing: return (int)(stopAfterCurrent ? GroupedView::State_StopAfter : GroupedView::State_Playing);
656                 case MPDState_Paused:  return (int)GroupedView::State_Paused;
657                 }
658             }  else if (-1!=s.id && s.id==stopAfterTrackId) {
659                 return GroupedView::State_StopAfterTrack;
660             }
661         }
662         return QVariant();
663     }
664     case Cantata::Role_Status: {
665         qint32 id=songs.at(index.row()).id;
666         if (id==currentSongId) {
667             switch (mpdState) {
668             case MPDState_Inactive:
669             case MPDState_Stopped: return (int)GroupedView::State_Stopped;
670             case MPDState_Playing: return (int)(stopAfterCurrent ? GroupedView::State_StopAfter : GroupedView::State_Playing);
671             case MPDState_Paused:  return (int)GroupedView::State_Paused;
672             }
673         } else if (-1!=id && id==stopAfterTrackId) {
674             return GroupedView::State_StopAfterTrack;
675         }
676         return (int)GroupedView::State_Default;
677         break;
678     }
679     case Qt::FontRole: {
680         Song s=songs.at(index.row());
681 
682         if (s.isStream()) {
683             QFont font;
684             if (songs.at(index.row()).id == currentSongId) {
685                 font.setBold(true);
686             }
687             font.setItalic(true);
688             return font;
689         }
690         else if (songs.at(index.row()).id == currentSongId) {
691             QFont font;
692             font.setBold(true);
693             return font;
694         }
695         break;
696     }
697 //     case Qt::BackgroundRole:
698 //         if (songs.at(index.row()).id == currentSongId) {
699 //             QColor col(QPalette().color(QPalette::Highlight));
700 //             col.setAlphaF(0.2);
701 //             return QVariant(col);
702 //         }
703 //         break;
704     case Qt::DisplayRole: {
705         const Song &song = songs.at(index.row());
706         switch (index.column()) {
707         case COL_TITLE:
708             return song.title.isEmpty() ? Utils::getFile(basicPath(song)) : song.title;
709         case COL_ARTIST:
710             return song.artist.isEmpty() ? Song::unknown() : song.artist;
711         case COL_ALBUM:
712             if (song.isStream() && song.album.isEmpty()) {
713                 QString n=song.name();
714                 if (!n.isEmpty()) {
715                     return n;
716                 }
717             }
718             return song.album;
719         case COL_TRACK:
720             if (song.track <= 0) {
721                 return QVariant();
722             }
723             return song.track;
724         case COL_LENGTH:
725             return Utils::formatTime(song.time);
726         case COL_DISC:
727             if (song.disc <= 0) {
728                 return QVariant();
729             }
730             return song.disc;
731         case COL_YEAR:
732             if (song.year <= 0) {
733                 return QVariant();
734             }
735             return song.year;
736         case COL_ORIGYEAR:
737             if (song.origYear <= 0) {
738                 return QVariant();
739             }
740             return song.origYear;
741         case COL_GENRE:
742             return song.displayGenre();
743         case COL_PRIO:
744             return song.priority;
745         case COL_COMPOSER:
746             return song.composer();
747         case COL_PERFORMER:
748             return song.performer();
749 //        case COL_RATING:{
750 //            QVariant var;
751 //            const Song &s=songs.at(index.row());
752 //            if (Song::Standard==s.type && Song::constNullRating==s.rating) {
753 //                emit getRating(s.file);
754 //                s.rating=Song::constRatingRequested;
755 //            }
756 //            var.setValue(s.rating);
757 //            return var;
758 //        }
759         case COL_FILENAME:
760             return Utils::getFile(QUrl(song.file).path());
761         case COL_PATH: {
762             QString dir=Utils::getDir(QUrl(song.file).path());
763             return dir.endsWith("/") ? dir.left(dir.length()-1) : dir;
764         }
765         default:
766             break;
767         }
768         break;
769     }
770     case Qt::ToolTipRole: {
771         if (!Settings::self()->infoTooltips()) {
772             return QVariant();
773         }
774         Song s=songs.at(index.row());
775         if (s.album.isEmpty() && s.isStream()) {
776             return basicPath(s);
777         } else {
778             return s.toolTip();
779         }
780     }
781     case Qt::TextAlignmentRole:
782         return alignments[index.column()];
783     case Cantata::Role_Decoration: {
784         qint32 id=songs.at(index.row()).id;
785         if (id==currentSongId) {
786             switch (mpdState) {
787             case MPDState_Inactive:
788             case MPDState_Stopped: return MonoIcon::icon(FontAwesome::stop, Utils::monoIconColor());
789             case MPDState_Playing: return MonoIcon::icon(stopAfterCurrent ? FontAwesome::playcircle : FontAwesome::play, Utils::monoIconColor());
790             case MPDState_Paused:  return MonoIcon::icon(FontAwesome::pause, Utils::monoIconColor());
791             }
792         } else if (-1!=id && id==stopAfterTrackId) {
793             return MonoIcon::icon(FontAwesome::stop, Utils::monoIconColor());
794         }
795         break;
796     }
797     default:
798         break;
799     }
800 
801     return QVariant();
802 }
803 
setData(const QModelIndex & index,const QVariant & value,int role)804 bool PlayQueueModel::setData(const QModelIndex &index, const QVariant &value, int role)
805 {
806     if (Cantata::Role_DropAdjust==role) {
807         dropAdjust=value.toUInt();
808         return true;
809     } else {
810         return QAbstractItemModel::setData(index, value, role);
811     }
812 }
813 
supportedDropActions() const814 Qt::DropActions PlayQueueModel::supportedDropActions() const
815 {
816     return Qt::CopyAction | Qt::MoveAction;
817 }
818 
flags(const QModelIndex & index) const819 Qt::ItemFlags PlayQueueModel::flags(const QModelIndex &index) const
820 {
821     if (index.isValid()) {
822         return Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsEnabled;
823     }
824     return Qt::ItemIsDropEnabled;
825 }
826 
827 /**
828  * @return A QStringList with the mimetypes we support
829  */
mimeTypes() const830 QStringList PlayQueueModel::mimeTypes() const
831 {
832     return QStringList() << constMoveMimeType << constFileNameMimeType << constUriMimeType;
833 }
834 
835 /**
836  * Convert the data at indexes into mimedata ready for transport
837  *
838  * @param indexes The indexes to pack into mimedata
839  * @return The mimedata
840  */
mimeData(const QModelIndexList & indexes) const841 QMimeData *PlayQueueModel::mimeData(const QModelIndexList &indexes) const
842 {
843     QMimeData *mimeData = new QMimeData();
844     QList<quint32> positions;
845     QStringList filenames;
846 
847     for (const QModelIndex &index: indexes) {
848         if (index.isValid() && 0==index.column()) {
849             positions.append(index.row());
850             filenames.append(static_cast<Song *>(index.internalPointer())->file);
851         }
852     }
853 
854     encode(*mimeData, constMoveMimeType, positions);
855     encode(*mimeData, constFileNameMimeType, filenames);
856     return mimeData;
857 }
858 
859 /**
860  * Act on mime data that is dropped in our model
861  *
862  * @param data The actual data that is dropped
863  * @param action The action. This could mean drop/copy etc
864  * @param row The row where it is dropper
865  * @param column The column where it is dropper
866  * @param parent The parent of where we have dropped it
867  *
868  * @return bool if we accest the drop
869  */
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int,const QModelIndex &)870 bool PlayQueueModel::dropMimeData(const QMimeData *data,
871                                   Qt::DropAction action, int row, int /*column*/, const QModelIndex & /*parent*/)
872 {
873     if (Qt::IgnoreAction==action) {
874         return true;
875     }
876 
877     row+=dropAdjust;
878     if (data->hasFormat(constMoveMimeType)) { //Act on internal moves
879         if (row < 0) {
880             emit move(decodeInts(*data, constMoveMimeType), songs.size(), songs.size());
881         } else {
882             emit move(decodeInts(*data, constMoveMimeType), row, songs.size());
883         }
884         return true;
885     } else if (data->hasFormat(constFileNameMimeType)) {
886         //Act on moves from the music library and dir view
887         addItems(decode(*data, constFileNameMimeType), row, false, 0, false);
888         return true;
889     } else if(data->hasFormat(constUriMimeType)/* && MPDConnection::self()->getDetails().isLocal()*/) {
890         QStringList useable=parseUrls(decode(*data, constUriMimeType));
891         if (useable.count()) {
892             addItems(useable, row, false, 0, false);
893             return true;
894         }
895     }
896     return false;
897 }
898 
load(const QStringList & urls,int action,quint8 priority,bool decreasePriority)899 void PlayQueueModel::load(const QStringList &urls, int action, quint8 priority, bool decreasePriority)
900 {
901     if (-1==action) {
902         action = songs.isEmpty() ? MPDConnection::AppendAndPlay : MPDConnection::Append;
903     }
904     QStringList useable=parseUrls(urls);
905     if (useable.count()) {
906         addItems(useable, songs.count(), action, priority, decreasePriority);
907     }
908 }
909 
addItems(const QStringList & items,int row,int action,quint8 priority,bool decreasePriority)910 void PlayQueueModel::addItems(const QStringList &items, int row, int action, quint8 priority, bool decreasePriority)
911 {
912     bool haveRadioStream=false;
913 
914     for (const QString &f: items) {
915         QUrl u(f);
916         if (u.scheme().startsWith(StreamsModel::constPrefix)) {
917             haveRadioStream=true;
918             break;
919         }
920     }
921 
922     if (haveRadioStream) {
923         emit fetchingStreams();
924         fetcher->get(items, row, action, priority, decreasePriority);
925     } else {
926         addFiles(items, row, action, priority, decreasePriority);
927     }
928 }
929 
addFiles(const QStringList & filenames,int row,int action,quint8 priority,bool decreasePriority)930 void PlayQueueModel::addFiles(const QStringList &filenames, int row, int action, quint8 priority, bool decreasePriority)
931 {
932     if (MPDConnection::ReplaceAndplay==action) {
933         emit filesAdded(filenames, 0, 0, MPDConnection::ReplaceAndplay, priority, decreasePriority);
934     } else if (songs.isEmpty()) {
935          emit filesAdded(filenames, 0, 0, action, priority, decreasePriority);
936     } else if (row < 0) {
937         emit filesAdded(filenames, songs.size(), songs.size(), action, priority, decreasePriority);
938     } else {
939         emit filesAdded(filenames, row, songs.size(), action, priority, decreasePriority);
940     }
941 }
942 
prioritySet(const QMap<qint32,quint8> & prio)943 void PlayQueueModel::prioritySet(const QMap<qint32, quint8> &prio)
944 {
945     QList<Song> prev;
946     if (undoEnabled) {
947         for (const Song &s: songs) {
948             prev.append(s);
949         }
950     }
951     QMap<qint32, quint8> copy = prio;
952     int row=0;
953 
954     for (const Song &s: songs) {
955         QMap<qint32, quint8>::ConstIterator it = copy.find(s.id);
956         if (copy.end()!=it) {
957             s.priority=it.value();
958             copy.remove(s.id);
959             QModelIndex idx(index(row, 0));
960             emit dataChanged(idx, idx);
961             if (copy.isEmpty()) {
962                 break;
963             }
964         }
965         ++row;
966     }
967 
968     saveHistory(prev);
969 }
970 
getIdByRow(qint32 row) const971 qint32 PlayQueueModel::getIdByRow(qint32 row) const
972 {
973     return row>=songs.size() ? -1 : songs.at(row).id;
974 }
975 
getSongId(const QString & file) const976 qint32 PlayQueueModel::getSongId(const QString &file) const
977 {
978     if (CueFile::isCue(file)) {
979         QUrl u(file);
980         QUrlQuery q(u);
981         Song check;
982         check.album=q.queryItemValue("album");
983         check.artist=q.queryItemValue("artist");
984         check.albumartist=q.queryItemValue("albumartist");
985         check.album=q.queryItemValue("album");
986         check.title=q.queryItemValue("title");
987         check.disc=q.queryItemValue("disc").toUInt();
988         check.track=q.queryItemValue("track").toUInt();
989         check.time=q.queryItemValue("time").toUInt();
990         check.year=q.queryItemValue("year").toUInt();
991         check.origYear=q.queryItemValue("origYear").toUInt();
992 
993         for (const Song &s: songs) {
994             if (s.sameMetadata(check)) {
995                 return s.id;
996             }
997         }
998     } else {
999         for (const Song &s: songs) {
1000             if (s.file==file) {
1001                 return s.id;
1002             }
1003         }
1004     }
1005 
1006     return -1;
1007 }
1008 
1009 // qint32 PlayQueueModel::getPosByRow(qint32 row) const
1010 // {
1011 //     return row>=songs.size() ? -1 : songs.at(row).pos;
1012 // }
1013 
getRowById(qint32 id) const1014 qint32 PlayQueueModel::getRowById(qint32 id) const
1015 {
1016     for (int i = 0; i < songs.size(); i++) {
1017         if (songs.at(i).id == id) {
1018             return i;
1019         }
1020     }
1021 
1022     return -1;
1023 }
1024 
getSongByRow(const qint32 row) const1025 Song PlayQueueModel::getSongByRow(const qint32 row) const
1026 {
1027     return row<0 || row>=songs.size() ? Song() : songs.at(row);
1028 }
1029 
getSongById(qint32 id) const1030 Song PlayQueueModel::getSongById(qint32 id) const
1031 {
1032     for (const Song &s: songs) {
1033         if (s.id==id) {
1034             return s;
1035         }
1036     }
1037     return Song();
1038 }
1039 
updateCurrentSong(quint32 id)1040 void PlayQueueModel::updateCurrentSong(quint32 id)
1041 {
1042     qint32 oldIndex = currentSongId;
1043     currentSongId = id;
1044 
1045     if (-1!=oldIndex) {
1046         int row=-1==currentSongRowNum ? getRowById(oldIndex) : currentSongRowNum;
1047         emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex())-1));
1048     }
1049 
1050     currentSongRowNum=getRowById(currentSongId);
1051     if (currentSongRowNum>=0 && currentSongRowNum<=songs.count()) {
1052         const Song &song=songs.at(currentSongRowNum);
1053         if (Song::Rating_Null==song.rating) {
1054             song.rating=Song::Rating_Requested;
1055             emit getRating(song.file);
1056         } else if (Song::Rating_Requested!=song.rating) {
1057             emit currentSongRating(song.file, song.rating);
1058         }
1059     }
1060     emit dataChanged(index(currentSongRowNum, 0), index(currentSongRowNum, columnCount(QModelIndex())-1));
1061 
1062     if (-1!=currentSongId && stopAfterTrackId==currentSongId) {
1063         stopAfterTrackId=-1;
1064         emit stop(true);
1065     }
1066 }
1067 
clear()1068 void PlayQueueModel::clear()
1069 {
1070     beginResetModel();
1071     songs.clear();
1072     ids.clear();
1073     currentSongId=-1;
1074     currentSongRowNum=0;
1075     stopAfterTrackId=-1;
1076     time=0;
1077     endResetModel();
1078 }
1079 
currentSongRow() const1080 qint32 PlayQueueModel::currentSongRow() const
1081 {
1082     if (-1==currentSongRowNum) {
1083         currentSongRowNum=getRowById(currentSongId);
1084     }
1085     return currentSongRowNum;
1086 }
1087 
setState(MPDState st)1088 void PlayQueueModel::setState(MPDState st)
1089 {
1090     if (st!=mpdState) {
1091         mpdState=st;
1092         if (-1!=currentSongId) {
1093             if (-1==currentSongRowNum) {
1094                 currentSongRowNum=getRowById(currentSongId);
1095             }
1096             emit dataChanged(index(currentSongRowNum, 0), index(currentSongRowNum, 2));
1097         }
1098     }
1099 }
1100 
1101 // Update playqueue with contents returned from MPD.
update(const QList<Song> & songList,bool isComplete)1102 void PlayQueueModel::update(const QList<Song> &songList, bool isComplete)
1103 {
1104     currentSongRowNum=-1;
1105     if (songList.isEmpty()) {
1106         Song::clearKeyStore(MPDParseUtils::Loc_PlayQueue);
1107     }
1108 
1109     removeDuplicatesAction->setEnabled(songList.count()>1);
1110     QList<Song> prev;
1111     if (undoEnabled) {
1112         prev=songs;
1113     }
1114 
1115     QSet<qint32> newIds;
1116     for (const Song &s: songList) {
1117         newIds.insert(s.id);
1118     }
1119 
1120     // If we have too many changes UI can hang, so it is sometimes better just to do a complete reset!
1121     if (isComplete && (songs.count()>MPDConnection::constMaxPqChanges
1122                        || !MPDConnection::self()->isPlayQueueIdValid())) {
1123         songs.clear();
1124     }
1125 
1126     if (songs.isEmpty() || songList.isEmpty()) {
1127         beginResetModel();
1128         songs=songList;
1129         ids=newIds;
1130         endResetModel();
1131         if (songList.isEmpty()) {
1132             stopAfterTrackId=-1;
1133         }
1134     } else {
1135         time = 0;
1136 
1137         QSet<qint32> removed=ids-newIds;
1138         for (qint32 id: removed) {
1139             qint32 row=getRowById(id);
1140             if (row!=-1) {
1141                 beginRemoveRows(QModelIndex(), row, row);
1142                 songs.removeAt(row);
1143                 endRemoveRows();
1144             }
1145         }
1146         for (qint32 i=0; i<songList.count(); ++i) {
1147             Song s=songList.at(i);
1148             bool newSong=i>=songs.count();
1149             Song currentSongAtPos=newSong ? Song() : songs.at(i);
1150             bool isEmpty=s.isEmpty();
1151 
1152             if (newSong || s.id!=currentSongAtPos.id) {
1153                 qint32 existingPos=newSong ? -1 : getRowById(s.id);
1154                 if (-1==existingPos) {
1155                     beginInsertRows(QModelIndex(), i, i);
1156                     songs.insert(i, s);
1157                     endInsertRows();
1158                 } else {
1159                     beginMoveRows(QModelIndex(), existingPos, existingPos, QModelIndex(), i>existingPos ? i+1 : i);
1160                     Song old=songs.takeAt(existingPos);
1161 //                     old.pos=s.pos;
1162                     s.rating=old.rating;
1163                     songs.insert(i, isEmpty ? old : s);
1164                     endMoveRows();
1165                 }
1166             } else if (isEmpty) {
1167                 s=currentSongAtPos;
1168             } else {
1169                 s.key=currentSongAtPos.key;
1170                 s.rating=currentSongAtPos.rating;
1171                 songs.replace(i, s);
1172                 if (s.title!=currentSongAtPos.title || s.artist!=currentSongAtPos.artist || s.name()!=currentSongAtPos.name()) {
1173                     emit dataChanged(index(i, 0), index(i, columnCount(QModelIndex())-1));
1174                 }
1175             }
1176 
1177             if (s.id==currentSongId) {
1178                 currentSongRowNum=i;
1179             }
1180             time += s.time;
1181         }
1182 
1183         if (songs.count()>songList.count()) {
1184             int toBeRemoved=songs.count()-songList.count();
1185             beginRemoveRows(QModelIndex(), songList.count(), songs.count()-1);
1186             for (int i=0; i<toBeRemoved; ++i) {
1187                 songs.takeLast();
1188             }
1189             endRemoveRows();
1190         }
1191 
1192         ids=newIds;
1193         if (-1!=stopAfterTrackId && !ids.contains(stopAfterTrackId)) {
1194             stopAfterTrackId=-1;
1195         }
1196         emit statsUpdated(songs.size(), time);
1197     }
1198 
1199     saveHistory(prev);
1200     shuffleAction->setEnabled(songs.count()>1);
1201     sortAction->setEnabled(songs.count()>1);
1202 }
1203 
setStopAfterTrack(qint32 track)1204 void PlayQueueModel::setStopAfterTrack(qint32 track)
1205 {
1206     bool clear=track==stopAfterTrackId || (track==currentSongId && stopAfterCurrent);
1207 
1208     stopAfterTrackId=clear ? -1 : track;
1209     qint32 row=getRowById(track);
1210     if (-1!=row) {
1211         emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex())-1));
1212     }
1213     if (clear) {
1214         emit clearStopAfter();
1215     } else if (stopAfterTrackId==currentSongId) {
1216         emit stop(true);
1217     }
1218 }
1219 
removeCantataStreams(bool cdOnly)1220 bool PlayQueueModel::removeCantataStreams(bool cdOnly)
1221 {
1222     QList<qint32> ids;
1223     for (const Song &s: songs) {
1224         if (s.isCdda() || (!cdOnly && s.isCantataStream())) {
1225             ids.append(s.id);
1226         }
1227     }
1228 
1229     if (!ids.isEmpty()) {
1230         emit removeSongs(ids);
1231         return true;
1232     }
1233     return false;
1234 }
1235 
removeAll()1236 void PlayQueueModel::removeAll()
1237 {
1238     emit clearEntries();
1239 }
1240 
remove(const QList<int> & rowsToRemove)1241 void PlayQueueModel::remove(const QList<int> &rowsToRemove)
1242 {
1243     QList<qint32> removeIds;
1244     for (const int &r: rowsToRemove) {
1245         if (r>-1 && r<songs.count()) {
1246             removeIds.append(songs.at(r).id);
1247         }
1248     }
1249 
1250     if (!removeIds.isEmpty()) {
1251         emit removeSongs(removeIds);
1252     }
1253 }
1254 
crop(const QList<int> & rowsToKeep)1255 void PlayQueueModel::crop(const QList<int> &rowsToKeep)
1256 {
1257     QSet<qint32> allIds;
1258     for (const Song &song: songs) {
1259         allIds.insert(song.id);
1260     }
1261 
1262     QSet<qint32> keepIds;
1263     for (const int &r: rowsToKeep) {
1264         if (r>-1 && r<songs.count()) {
1265             keepIds.insert(songs.at(r).id);
1266         }
1267     }
1268 
1269     QSet<qint32> removeIds=allIds-keepIds;
1270     if (!removeIds.isEmpty()) {
1271         emit removeSongs(removeIds.toList());
1272     }
1273 }
1274 
setRating(const QList<int> & rows,quint8 rating) const1275 void PlayQueueModel::setRating(const QList<int> &rows, quint8 rating) const
1276 {
1277     QSet<QString> files;
1278     for (const int &r: rows) {
1279         if (r>-1 && r<songs.count()) {
1280             const Song &s=songs.at(r);
1281             if (Song::Standard==s.type && !files.contains(s.file)) {
1282                 files.insert(s.file);
1283             }
1284         }
1285     }
1286     emit setRating(files.toList(), rating);
1287 }
1288 
enableUndo(bool e)1289 void PlayQueueModel::enableUndo(bool e)
1290 {
1291     if (e==undoEnabled) {
1292         return;
1293     }
1294     undoEnabled=e && undoLimit>0;
1295     if (!e) {
1296         undoStack.clear();
1297         redoStack.clear();
1298     }
1299     controlActions();
1300 }
1301 
getState(const QList<Song> & songs)1302 static PlayQueueModel::UndoItem getState(const QList<Song> &songs)
1303 {
1304     PlayQueueModel::UndoItem item;
1305     for (const Song &s: songs) {
1306         item.files.append(s.file);
1307         item.priority.append(s.priority);
1308     }
1309     return item;
1310 }
1311 
equalSongList(const QList<Song> & a,const QList<Song> & b)1312 static bool equalSongList(const QList<Song> &a, const QList<Song> &b)
1313 {
1314     if (a.count()!=b.count()) {
1315         return false;
1316     }
1317 
1318     for (int i=0; i<a.count(); ++i) {
1319         const Song &sa=a.at(i);
1320         const Song &sb=b.at(i);
1321         if (sa.priority!=sb.priority || sa.file!=sb.file) {
1322             return false;
1323         }
1324     }
1325     return true;
1326 }
1327 
saveHistory(const QList<Song> & prevList)1328 void PlayQueueModel::saveHistory(const QList<Song> &prevList)
1329 {
1330     if (!undoEnabled) {
1331         return;
1332     }
1333 
1334     if (equalSongList(prevList, songs)) {
1335         lastCommand=Cmd_Other;
1336         return;
1337     }
1338 
1339     switch (lastCommand) {
1340     case Cmd_Redo: {
1341         if (redoStack.isEmpty()) {
1342             lastCommand=Cmd_Other;
1343         } else {
1344             UndoItem actioned=redoStack.pop();
1345             if (actioned!=getState(songs)) {
1346                 lastCommand=Cmd_Other;
1347             } else {
1348                 undoStack.push(getState(prevList));
1349             }
1350         }
1351         break;
1352     }
1353     case Cmd_Undo: {
1354         if (undoStack.isEmpty()) {
1355             lastCommand=Cmd_Other;
1356         } else {
1357             UndoItem actioned=undoStack.pop();
1358             if (actioned!=getState(songs)) {
1359                 lastCommand=Cmd_Other;
1360             } else {
1361                 redoStack.push(getState(prevList));
1362             }
1363         }
1364         break;
1365     }
1366     case Cmd_Other:
1367         break;
1368     }
1369 
1370     if (Cmd_Other==lastCommand) {
1371         redoStack.clear();
1372         undoStack.push(getState(prevList));
1373         if (undoStack.size()>undoLimit) {
1374             undoStack.pop_back();
1375         }
1376     }
1377 
1378     controlActions();
1379     lastCommand=Cmd_Other;
1380 }
1381 
controlActions()1382 void PlayQueueModel::controlActions()
1383 {
1384     undoAction->setEnabled(!undoStack.isEmpty());
1385     undoAction->setVisible(undoLimit>0);
1386     redoAction->setEnabled(!redoStack.isEmpty());
1387     redoAction->setVisible(undoLimit>0);
1388 }
1389 
addSortAction(const QString & name,const QString & key)1390 void PlayQueueModel::addSortAction(const QString &name, const QString &key)
1391 {
1392     Action *action=new Action(name, sortAction);
1393     action->setProperty(constSortByKey, key);
1394     sortAction->menu()->addAction(action);
1395     connect(action, SIGNAL(triggered()), SLOT(sortBy()));
1396 }
1397 
composerSort(const Song * s1,const Song * s2)1398 static bool composerSort(const Song *s1, const Song *s2)
1399 {
1400     const QString v1=s1->hasComposer() ? s1->composer() : QString();
1401     const QString v2=s2->hasComposer() ? s2->composer() : QString();
1402     int c=v1.localeAwareCompare(v2);
1403     return c<0 || (c==0 && (*s1)<(*s2));
1404 }
1405 
performerSort(const Song * s1,const Song * s2)1406 static bool performerSort(const Song *s1, const Song *s2)
1407 {
1408     const QString v1=s1->hasPerformer() ? s1->performer() : QString();
1409     const QString v2=s2->hasPerformer() ? s2->performer() : QString();
1410     int c=v1.localeAwareCompare(v2);
1411     return c<0 || (c==0 && (*s1)<(*s2));
1412 }
1413 
artistSort(const Song * s1,const Song * s2)1414 static bool artistSort(const Song *s1, const Song *s2)
1415 {
1416     const QString v1=s1->hasArtistSort() ? s1->artistSort() : s1->artist;
1417     const QString v2=s2->hasArtistSort() ? s2->artistSort() : s2->artist;
1418     int c=v1.localeAwareCompare(v2);
1419     return c<0 || (c==0 && (*s1)<(*s2));
1420 }
1421 
albumArtistSort(const Song * s1,const Song * s2)1422 static bool albumArtistSort(const Song *s1, const Song *s2)
1423 {
1424     const QString v1=s1->hasAlbumArtistSort() ? s1->albumArtistSort() : s1->albumArtistOrComposer();
1425     const QString v2=s2->hasAlbumArtistSort() ? s2->albumArtistSort() : s2->albumArtistOrComposer();
1426     int c=v1.localeAwareCompare(v2);
1427     return c<0 || (c==0 && (*s1)<(*s2));
1428 }
1429 
albumSort(const Song * s1,const Song * s2)1430 static bool albumSort(const Song *s1, const Song *s2)
1431 {
1432     const QString v1=s1->hasAlbumSort() ? s1->albumSort() : s1->album;
1433     const QString v2=s2->hasAlbumSort() ? s2->albumSort() : s2->album;
1434     int c=v1.localeAwareCompare(v2);
1435     return c<0 || (c==0 && (*s1)<(*s2));
1436 }
1437 
genreSort(const Song * s1,const Song * s2)1438 static bool genreSort(const Song *s1, const Song *s2)
1439 {
1440     int c=s1->compareGenres(*s2);
1441     return c<0 || (c==0 && (*s1)<(*s2));
1442 }
1443 
yearSort(const Song * s1,const Song * s2)1444 static bool yearSort(const Song *s1, const Song *s2)
1445 {
1446     return s1->year<s2->year || (s1->year==s2->year && (*s1)<(*s2));
1447 }
1448 
titleSort(const Song * s1,const Song * s2)1449 static bool titleSort(const Song *s1, const Song *s2)
1450 {
1451     int c=s1->title.localeAwareCompare(s2->title);
1452     return c<0 || (c==0 && (*s1)<(*s2));
1453 }
1454 
trackSort(const Song * s1,const Song * s2)1455 static bool trackSort(const Song *s1, const Song *s2)
1456 {
1457     return s1->track<s2->track || (s1->track==s2->track && (*s1)<(*s2));
1458 }
1459 
sortBy()1460 void PlayQueueModel::sortBy()
1461 {
1462     Action *act=qobject_cast<Action *>(sender());
1463     if (act) {
1464         QString key=act->property(constSortByKey).toString();
1465         QList<const Song *> copy;
1466         for (const Song &s: songs) {
1467             ((Song &)s).populateSorts();
1468             copy.append(&s);
1469         }
1470 
1471         if (constSortByArtistKey==key) {
1472             std::sort(copy.begin(), copy.end(), artistSort);
1473         } else if (constSortByAlbumArtistKey==key) {
1474             std::sort(copy.begin(), copy.end(), albumArtistSort);
1475         } else if (constSortByAlbumKey==key) {
1476             std::sort(copy.begin(), copy.end(), albumSort);
1477         } else if (constSortByGenreKey==key) {
1478             std::sort(copy.begin(), copy.end(), genreSort);
1479         } else if (constSortByYearKey==key) {
1480             std::sort(copy.begin(), copy.end(), yearSort);
1481         } else if (constSortByComposerKey==key) {
1482             std::sort(copy.begin(), copy.end(), composerSort);
1483         } else if (constSortByPerformerKey==key) {
1484             std::sort(copy.begin(), copy.end(), performerSort);
1485         } else if (constSortByTitleKey==key) {
1486             std::sort(copy.begin(), copy.end(), titleSort);
1487         } else if (constSortByNumberKey==key) {
1488             std::sort(copy.begin(), copy.end(), trackSort);
1489         }
1490         QList<quint32> positions;
1491         for (const Song *s: copy) {
1492             positions.append(getRowById(s->id));
1493         }
1494         emit setOrder(positions);
1495     }
1496 }
1497 
removeDuplicates()1498 void PlayQueueModel::removeDuplicates()
1499 {
1500     QMap<QString, QList<Song> > map;
1501     for (const Song &song: songs) {
1502         map[song.albumKey()+"-"+song.artistSong()+"-"+song.track].append(song);
1503     }
1504 
1505     QList<qint32> toRemove;
1506     for (const QString &key: map.keys()) {
1507         QList<Song> values=map.value(key);
1508         if (values.size()>1) {
1509             Song::sortViaType(values);
1510             for (int i=1; i<values.count(); ++i) {
1511                 toRemove.append(values.at(i).id);
1512             }
1513         }
1514     }
1515     if (!toRemove.isEmpty()) {
1516         emit removeSongs(toRemove);
1517     }
1518 }
1519 
ratingResult(const QString & file,quint8 r)1520 void PlayQueueModel::ratingResult(const QString &file, quint8 r)
1521 {
1522     QList<Song>::iterator it=songs.begin();
1523     QList<Song>::iterator end=songs.end();
1524     int numCols=columnCount(QModelIndex())-1;
1525 
1526     for (int row=0; it!=end; ++it, ++row) {
1527         if (Song::Standard==(*it).type && r!=(*it).rating && (*it).file==file) {
1528             (*it).rating=r;
1529             emit dataChanged(index(row, 0), index(row, numCols));
1530             if ((*it).id==currentSongId) {
1531                 emit currentSongRating(file, r);
1532             }
1533         }
1534     }
1535 }
1536 
stickerDbChanged()1537 void PlayQueueModel::stickerDbChanged()
1538 {
1539     // Sticker DB changed, need to re-request ratings...
1540     QSet<QString> requests;
1541     for (const Song &song: songs) {
1542         if (Song::Standard==song.type && song.rating<=Song::Rating_Max && !requests.contains(song.file)) {
1543             emit getRating(song.file);
1544             requests.insert(song.file);
1545         }
1546     }
1547 }
1548 
undo()1549 void PlayQueueModel::undo()
1550 {
1551     if (!undoEnabled || undoStack.isEmpty()) {
1552         return;
1553     }
1554     UndoItem item=undoStack.top();
1555     emit populate(item.files, item.priority);
1556     lastCommand=Cmd_Undo;
1557 }
1558 
redo()1559 void PlayQueueModel::redo()
1560 {
1561     if (!undoEnabled || redoStack.isEmpty()) {
1562         return;
1563     }
1564     UndoItem item=redoStack.top();
1565     emit populate(item.files, item.priority);
1566     lastCommand=Cmd_Redo;
1567 }
1568 
playSong(const QString & file)1569 void PlayQueueModel::playSong(const QString &file)
1570 {
1571     qint32 id=getSongId(file);
1572     if (-1==id) {
1573         emit addAndPlay(file);
1574     } else {
1575         emit startPlayingSongId(id);
1576     }
1577 }
1578 
stats()1579 void PlayQueueModel::stats()
1580 {
1581     time = 0;
1582     //Loop over all songs
1583     for (const Song &song: songs) {
1584         time += song.time;
1585     }
1586 
1587     emit statsUpdated(songs.size(), time);
1588 }
1589 
cancelStreamFetch()1590 void PlayQueueModel::cancelStreamFetch()
1591 {
1592     fetcher->cancel();
1593 }
1594 
songSort(const Song * s1,const Song * s2)1595 static bool songSort(const Song *s1, const Song *s2)
1596 {
1597     return s1->disc<s2->disc || (s1->disc==s2->disc && (s1->track<s2->track || (s1->track==s2->track && (*s1)<(*s2))));
1598 }
1599 
shuffleAlbums()1600 void PlayQueueModel::shuffleAlbums()
1601 {
1602     QMap<quint32, QList<const Song *> > albums;
1603     for (const Song &s: songs) {
1604         albums[s.key].append(&s);
1605     }
1606 
1607     QList<quint32> keys=albums.keys();
1608     if (keys.count()<2) {
1609         return;
1610     }
1611 
1612     QList<quint32> positions;
1613     while (!keys.isEmpty()) {
1614         quint32 key=keys.takeAt(QRandomGenerator::global()->bounded(keys.count()));
1615         QList<const Song *> albumSongs=albums[key];
1616         std::sort(albumSongs.begin(), albumSongs.end(), songSort);
1617         for (const Song *song: albumSongs) {
1618             positions.append(getRowById(song->id));
1619         }
1620     }
1621     emit setOrder(positions);
1622 }
1623 
stopAfterCurrentChanged(bool afterCurrent)1624 void PlayQueueModel::stopAfterCurrentChanged(bool afterCurrent)
1625 {
1626     if (afterCurrent!=stopAfterCurrent) {
1627         stopAfterCurrent=afterCurrent;
1628         emit dataChanged(index(currentSongRowNum, 0), index(currentSongRowNum, columnCount(QModelIndex())-1));
1629     }
1630 }
1631 
remove(const QList<Song> & rem)1632 void PlayQueueModel::remove(const QList<Song> &rem)
1633 {
1634     QSet<QString> s;
1635     QList<qint32> ids;
1636 
1637     for (const Song &song: rem) {
1638         s.insert(song.file);
1639     }
1640 
1641     for (const Song &song: songs) {
1642         if (s.contains(song.file)) {
1643             ids.append(song.id);
1644             s.remove(song.file);
1645             if (s.isEmpty()) {
1646                 break;
1647             }
1648         }
1649     }
1650 
1651     if (!ids.isEmpty()) {
1652         emit removeSongs(ids);
1653     }
1654 }
1655 
updateDetails(const QList<Song> & updated)1656 void PlayQueueModel::updateDetails(const QList<Song> &updated)
1657 {
1658     QMap<QString, Song> songMap;
1659     QList<int> updatedRows;
1660     bool currentUpdated=false;
1661     Song currentSong;
1662 
1663     for (const Song &song: updated) {
1664         songMap[song.file]=song;
1665     }
1666 
1667     for (int i=0; i<songs.count(); ++i) {
1668         Song current=songs.at(i);
1669         if (songMap.contains(current.file)) {
1670             Song updatedSong=songMap[current.file];
1671             updatedSong.id=current.id;
1672             updatedSong.setKey(MPDParseUtils::Loc_PlayQueue);
1673 
1674             if (updatedSong.title!=current.title || updatedSong.artist!=current.artist || updatedSong.name()!=current.name()) {
1675                 songs.replace(i, updatedSong);
1676                 updatedRows.append(i);
1677                 if (currentSongId==current.id) {
1678                     currentUpdated=true;
1679                     currentSong=updatedSong;
1680                 }
1681             }
1682 
1683             songMap.remove(current.file);
1684             if (songMap.isEmpty()) {
1685                 break;
1686             }
1687         }
1688     }
1689 
1690     if (!updatedRows.isEmpty()) {
1691         if (updatedRows.count()==updated.count()) {
1692             beginResetModel();
1693             endResetModel();
1694         } else {
1695             for (int row: updatedRows) {
1696                 emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex())-1));
1697             }
1698         }
1699     }
1700 
1701     if (currentUpdated) {
1702         emit updateCurrent(currentSong);
1703     }
1704 }
1705 
filenames()1706 QStringList PlayQueueModel::filenames()
1707 {
1708     QStringList names;
1709     for (const Song &song: songs) {
1710         names << song.file;
1711     }
1712     return names;
1713 }
1714 
1715 #include "moc_playqueuemodel.cpp"
1716