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