From fc88de90bc7b62a22fb79e1f67b932d4022e92c1 Mon Sep 17 00:00:00 2001 From: Craig Drummond Date: Fri, 18 Aug 2017 22:45:18 +0100 Subject: [PATCH] Initial implementation of smart playlists. NOT complete!!! --- CMakeLists.txt | 12 +- ChangeLog | 1 + cantata.qrc | 1 + icons/gradcap.svg | 13 + mpd-interface/mpdconnection.cpp | 48 ++++ mpd-interface/mpdconnection.h | 2 + mpd-interface/mpdparseutils.cpp | 23 ++ mpd-interface/mpdparseutils.h | 6 + playlists/cantata-dynamic | 2 + playlists/dynamicplaylists.cpp | 263 +------------------- playlists/dynamicplaylists.h | 58 +---- playlists/dynamicplaylistspage.cpp | 6 +- playlists/dynamicplaylistspage.h | 4 +- playlists/playlistproxymodel.cpp | 18 +- playlists/playlistrule.ui | 2 +- playlists/playlistruledialog.cpp | 84 ++++--- playlists/playlistruledialog.h | 12 +- playlists/playlistrules.ui | 2 +- playlists/playlistrulesdialog.cpp | 129 ++++++---- playlists/playlistrulesdialog.h | 5 +- playlists/playlistspage.cpp | 5 +- playlists/playlistspage.h | 2 + playlists/rulesplaylists.cpp | 306 +++++++++++++++++++++++ playlists/rulesplaylists.h | 115 +++++++++ playlists/smartplaylists.cpp | 70 ++++++ playlists/smartplaylists.h | 50 ++++ playlists/smartplaylistspage.cpp | 382 +++++++++++++++++++++++++++++ playlists/smartplaylistspage.h | 103 ++++++++ translations/blank.ts | 27 +- translations/cantata_cs.ts | 31 +-- translations/cantata_de.ts | 29 ++- translations/cantata_en_GB.ts | 29 ++- translations/cantata_es.ts | 29 ++- translations/cantata_fr.ts | 29 ++- translations/cantata_hu.ts | 27 +- translations/cantata_it.ts | 29 ++- translations/cantata_ja.ts | 27 +- translations/cantata_ko.ts | 27 +- translations/cantata_pl.ts | 46 ++-- translations/cantata_ru.ts | 31 +-- translations/cantata_zh_CN.ts | 27 +- 41 files changed, 1521 insertions(+), 591 deletions(-) create mode 100644 icons/gradcap.svg create mode 100644 playlists/rulesplaylists.cpp create mode 100644 playlists/rulesplaylists.h create mode 100644 playlists/smartplaylists.cpp create mode 100644 playlists/smartplaylists.h create mode 100644 playlists/smartplaylistspage.cpp create mode 100644 playlists/smartplaylistspage.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 11656bf88..4ff379e14 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -209,10 +209,8 @@ find_package(Qt5Concurrent REQUIRED) find_package(Qt5Svg REQUIRED) find_package(Qt5Sql REQUIRED) -if (${Qt5Widgets_VERSION} VERSION_GREATER "5.6.99") - set(CMAKE_CXX_FLAGS "-std=c++11 ${CMAKE_CXX_FLAGS}") - set(CMAKE_CXX_STANDARD "11") -endif () +set(CMAKE_CXX_FLAGS "-std=c++11 ${CMAKE_CXX_FLAGS}") +set(CMAKE_CXX_STANDARD 11) set(QTCORELIBS ${Qt5Core_LIBRARIES}) set(QTNETWORKLIBS ${Qt5Network_LIBRARIES}) @@ -330,7 +328,8 @@ set(CANTATA_SRCS ${CANTATA_SRCS} mpd-interface/song.cpp mpd-interface/cuefile.cpp network/networkaccessmanager.cpp network/networkproxyfactory.cpp playlists/dynamicplaylists.cpp playlists/playlistproxymodel.cpp playlists/dynamicplaylistspage.cpp playlists/playlistruledialog.cpp - playlists/playlistrulesdialog.cpp playlists/playlistspage.cpp playlists/storedplaylistspage.cpp + playlists/playlistrulesdialog.cpp playlists/playlistspage.cpp playlists/storedplaylistspage.cpp playlists/rulesplaylists.cpp + playlists/smartplaylists.cpp playlists/smartplaylistspage.cpp online/onlineservicespage.cpp online/onlinedbservice.cpp online/jamendoservice.cpp online/onlinedbwidget.cpp online/onlineservice.cpp online/jamendosettingsdialog.cpp online/magnatuneservice.cpp online/magnatunesettingsdialog.cpp online/soundcloudservice.cpp online/onlinesearchwidget.cpp online/podcastservice.cpp online/rssparser.cpp online/opmlparser.cpp online/podcastsearchdialog.cpp @@ -367,7 +366,8 @@ set(CANTATA_MOC_HDRS ${CANTATA_MOC_HDRS} online/onlinesearchservice.h db/onlinedb.h playlists/dynamicplaylists.h playlists/playlistruledialog.h playlists/dynamicplaylistspage.h playlists/playlistrulesdialog.h - playlists/playlistspage.h playlists/storedplaylistspage.h + playlists/playlistspage.h playlists/storedplaylistspage.h playlists/rulesplaylists.h playlists/smartplaylists.h + playlists/smartplaylistspage.h scrobbling/scrobbler.h scrobbling/scrobblingsettings.h scrobbling/scrobblingstatus.h scrobbling/scrobblinglove.h) set(CANTATA_UIS ${CANTATA_UIS} gui/initialsettingswizard.ui gui/mainwindow.ui diff --git a/ChangeLog b/ChangeLog index 4ee9a32d5..a964ad76a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -17,6 +17,7 @@ 11. In playlists page, internet, etc, allow back navigation to go fully back. 12. Don't try to seek if no song loaded. 13. Only use menubar for macOS builds. +14. Smart playlists - like dynamic, but do not auto update. 2.1.0 ----- diff --git a/cantata.qrc b/cantata.qrc index b972c21ee..ea2d7208e 100644 --- a/cantata.qrc +++ b/cantata.qrc @@ -33,6 +33,7 @@ icons/dice.svg icons/playlist.svg icons/radio.svg +icons/gradcap.svg diff --git a/icons/gradcap.svg b/icons/gradcap.svg new file mode 100644 index 000000000..325e1ec66 --- /dev/null +++ b/icons/gradcap.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/mpd-interface/mpdconnection.cpp b/mpd-interface/mpdconnection.cpp index af8dad2fb..2f14264fb 100644 --- a/mpd-interface/mpdconnection.cpp +++ b/mpd-interface/mpdconnection.cpp @@ -1830,6 +1830,54 @@ void MPDConnection::search(const QString &field, const QString &value, int id) emit searchResponse(id, songs); } +void MPDConnection::search(const QByteArray &query, const QString &id) +{ + QList songs; + if (query.isEmpty()) { + Response response=sendCommand("list albumartist", false, false); + if (response.ok) { + QList lines = response.data.split('\n'); + foreach (const QByteArray &line, lines) { + if (line.startsWith("AlbumArtist: ")) { + Response resp = sendCommand("find albumartist " + encodeName(QString::fromUtf8(line.mid(13))) , false, false); + if (resp.ok) { + songs += MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_Search); + } + } + } + } + } else if (query.startsWith("RATING:")) { + QList parts = query.split(':'); + if (3==parts.length()) { + Response response=sendCommand("sticker find song \"\" rating", false, false); + if (response.ok) { + int min = parts.at(1).toInt(); + int max = parts.at(2).toInt(); + QList stickers=MPDParseUtils::parseStickers(response.data, constRatingSticker); + if (!stickers.isEmpty()) { + foreach (const MPDParseUtils::Sticker &sticker, stickers) { + if (!sticker.file.isEmpty() && !sticker.value.isEmpty()) { + int val = sticker.value.toInt(); + if (val>=min && val<=max) { + Response resp = sendCommand("find file " + encodeName(QString::fromUtf8(sticker.file)) , false, false); + if (resp.ok) { + songs.append(MPDParseUtils::parseSong(response.data, MPDParseUtils::Loc_Search)); + } + } + } + } + } + } + } + } else { + Response response=sendCommand(query); + if (response.ok) { + songs=MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_Search); + } + } + emit searchResponse(id, songs); +} + void MPDConnection::listStreams() { Response response=sendCommand("listplaylistinfo "+encodeName(constStreamsPlayListName), false); diff --git a/mpd-interface/mpdconnection.h b/mpd-interface/mpdconnection.h index afb1da9c2..ef66ca952 100644 --- a/mpd-interface/mpdconnection.h +++ b/mpd-interface/mpdconnection.h @@ -310,6 +310,7 @@ public Q_SLOTS: void setPriority(const QList &ids, quint8 priority, bool decreasePriority); void search(const QString &field, const QString &value, int id); + void search(const QByteArray &query, const QString &id); void listStreams(); void saveStream(const QString &url, const QString &name); @@ -361,6 +362,7 @@ Q_SIGNALS: void streamUrl(const QString &url); void searchResponse(int id, const QList &songs); + void searchResponse(const QString &id, const QList &songs); void socketAddress(const QString &addr); void cantataStreams(const QStringList &files); diff --git a/mpd-interface/mpdparseutils.cpp b/mpd-interface/mpdparseutils.cpp index e15ae6528..a1ff6351a 100644 --- a/mpd-interface/mpdparseutils.cpp +++ b/mpd-interface/mpdparseutils.cpp @@ -788,6 +788,29 @@ QByteArray MPDParseUtils::parseSticker(const QByteArray &data, const QByteArray return QByteArray(); } +QList MPDParseUtils::parseStickers(const QByteArray &data, const QByteArray &sticker) +{ + QList stickers; + QList lines = data.split('\n'); + Sticker s; + QByteArray key=constSticker+sticker+'='; + + foreach (const QByteArray &line, lines) { + if (constOkValue==line) { + break; + } + + if (line.startsWith(constFileKey)) { + s.file=line.mid(constFileKey.length()); + } else if (line.startsWith(key)) { + s.value=line.mid(key.length()); + stickers.append(s); + } + } + + return stickers; +} + QString MPDParseUtils::addStreamName(const QString &url, const QString &name) { return name.isEmpty() diff --git a/mpd-interface/mpdparseutils.h b/mpd-interface/mpdparseutils.h index 716da1f15..4a1148754 100644 --- a/mpd-interface/mpdparseutils.h +++ b/mpd-interface/mpdparseutils.h @@ -67,6 +67,11 @@ namespace MPDParseUtils Cue_Count }; + struct Sticker { + QByteArray file; + QByteArray value; + }; + extern QString toStr(CueSupport cs); extern CueSupport toCueSupport(const QString &cs); extern void setCueFileSupport(CueSupport cs); @@ -85,6 +90,7 @@ namespace MPDParseUtils extern void parseDirItems(const QByteArray &data, const QString &mpdDir, long mpdVersion, QList &songList, const QString &dir, QStringList &subDirs, Location loc); extern QList parseOuputs(const QByteArray &data); extern QByteArray parseSticker(const QByteArray &data, const QByteArray &sticker); + extern QList parseStickers(const QByteArray &data, const QByteArray &sticker); extern QString addStreamName(const QString &url, const QString &name); extern QString getStreamName(const QString &url); extern QString getAndRemoveStreamName(QString &url); diff --git a/playlists/cantata-dynamic b/playlists/cantata-dynamic index 5eadce892..deed8cefe 100755 --- a/playlists/cantata-dynamic +++ b/playlists/cantata-dynamic @@ -440,6 +440,8 @@ sub readRules() { @dates=(); @similarArtists=(); @genres=(); + $isInclude=1; + $ruleMatch="find"; } elsif ($key=~ m/^(Rating)/) { my @vals = split("-", $val); if (scalar(@vals)==2) { diff --git a/playlists/dynamicplaylists.cpp b/playlists/dynamicplaylists.cpp index 6a892c79f..881dbe380 100644 --- a/playlists/dynamicplaylists.cpp +++ b/playlists/dynamicplaylists.cpp @@ -50,8 +50,6 @@ void DynamicPlaylists::enableDebug() debugEnabled=true; } -static const QString constDir=QLatin1String("dynamic"); -static const QString constExtension=QLatin1String(".rules"); static const QString constActiveRules=QLatin1String("rules"); static const QString constLockFile=QLatin1String("lock"); @@ -123,30 +121,12 @@ QString DynamicPlaylists::toString(Command cmd) GLOBAL_STATIC(DynamicPlaylists, instance) -const QString DynamicPlaylists::constRuleKey=QLatin1String("Rule"); -const QString DynamicPlaylists::constArtistKey=QLatin1String("Artist"); -const QString DynamicPlaylists::constSimilarArtistsKey=QLatin1String("SimilarArtists"); -const QString DynamicPlaylists::constAlbumArtistKey=QLatin1String("AlbumArtist"); -const QString DynamicPlaylists::constComposerKey=QLatin1String("Composer"); -const QString DynamicPlaylists::constCommentKey=QLatin1String("Comment"); -const QString DynamicPlaylists::constAlbumKey=QLatin1String("Album"); -const QString DynamicPlaylists::constTitleKey=QLatin1String("Title"); -const QString DynamicPlaylists::constGenreKey=QLatin1String("Genre"); -const QString DynamicPlaylists::constDateKey=QLatin1String("Date"); -const QString DynamicPlaylists::constRatingKey=QLatin1String("Rating"); -const QString DynamicPlaylists::constDurationKey=QLatin1String("Duration"); -const QString DynamicPlaylists::constNumTracksKey=QLatin1String("NumTracks"); -const QString DynamicPlaylists::constFileKey=QLatin1String("File"); -const QString DynamicPlaylists::constExactKey=QLatin1String("Exact"); -const QString DynamicPlaylists::constExcludeKey=QLatin1String("Exclude"); -const QChar DynamicPlaylists::constRangeSep=QLatin1Char('-'); - -const QChar constKeyValSep=QLatin1Char(':'); const QString constOk=QLatin1String("0"); const QString constFilename=QLatin1String("FILENAME:"); DynamicPlaylists::DynamicPlaylists() - : localTimer(0) + : RulesPlaylists("dice", "dynamic") + , localTimer(0) , usingRemote(false) , remoteTimer(0) , remotePollingEnabled(false) @@ -154,8 +134,6 @@ DynamicPlaylists::DynamicPlaylists() , currentJob(0) , currentCommand(Unknown) { - icn.addFile(":dice.svg"); - loadLocal(); connect(this, SIGNAL(clear()), MPDConnection::self(), SLOT(clear())); connect(MPDConnection::self(), SIGNAL(dynamicSupport(bool)), this, SLOT(remoteDynamicSupported(bool))); connect(this, SIGNAL(remoteMessage(QStringList)), MPDConnection::self(), SLOT(sendDynamicMessage(QStringList))); @@ -180,49 +158,12 @@ QString DynamicPlaylists::descr() const return tr("Dynamically generated playlists"); } -QVariant DynamicPlaylists::headerData(int, Qt::Orientation, int) const -{ - return QVariant(); -} - -int DynamicPlaylists::rowCount(const QModelIndex &parent) const -{ - return parent.isValid() ? 0 : entryList.count(); -} - -bool DynamicPlaylists::hasChildren(const QModelIndex &parent) const -{ - return !parent.isValid(); -} - -QModelIndex DynamicPlaylists::parent(const QModelIndex &) const -{ - return QModelIndex(); -} - -QModelIndex DynamicPlaylists::index(int row, int column, const QModelIndex &parent) const -{ - if (parent.isValid() || !hasIndex(row, column, parent) || row>=entryList.count()) { - return QModelIndex(); - } - - return createIndex(row, column); -} - #define IS_ACTIVE(E) !currentEntry.isEmpty() && (E)==currentEntry && (!isRemote() || QLatin1String("IDLE")!=lastState) QVariant DynamicPlaylists::data(const QModelIndex &index, int role) const { if (!index.isValid()) { - switch (role) { - case Cantata::Role_TitleText: - return title(); - case Cantata::Role_SubText: - return descr(); - case Qt::DecorationRole: - return icon(); - } - return QVariant(); + return RulesPlaylists::data(index, role); } if (index.parent().isValid() || index.row()>=entryList.count()) { @@ -230,134 +171,35 @@ QVariant DynamicPlaylists::data(const QModelIndex &index, int role) const } switch (role) { - case Qt::ToolTipRole: - if (!Settings::self()->infoTooltips()) { - return QVariant(); - } - case Qt::DisplayRole: - return entryList.at(index.row()).name; case Qt::DecorationRole: return IS_ACTIVE(entryList.at(index.row()).name) ? Icons::self()->replacePlayQueueIcon : Icons::self()->dynamicListIcon; - case Cantata::Role_SubText: { - const Entry &e=entryList.at(index.row()); - return tr("%n Rule(s)", "", e.rules.count())+(e.haveRating() ? tr(" - Rating: %1..%2") - .arg((double)e.ratingFrom/Song::Rating_Step).arg((double)e.ratingTo/Song::Rating_Step) : QString()); - } case Cantata::Role_Actions: { QVariant v; v.setValue >(QList() << (IS_ACTIVE(entryList.at(index.row()).name) ? stopAction : startAction)); return v; } default: - return QVariant(); + return RulesPlaylists::data(index, role); } } -Qt::ItemFlags DynamicPlaylists::flags(const QModelIndex &index) const +bool DynamicPlaylists::saveRemote(const QString &string, const Entry &e) { - if (index.isValid()) { - return Qt::ItemIsSelectable | Qt::ItemIsEnabled; - } - return Qt::NoItemFlags; -} - -DynamicPlaylists::Entry DynamicPlaylists::entry(const QString &e) -{ - if (!e.isEmpty()) { - QList::Iterator it=find(e); - if (it!=entryList.end()) { - return *it; - } - } - - return Entry(); -} - -bool DynamicPlaylists::save(const Entry &e) -{ - if (e.name.isEmpty()) { - return false; - } - - QString string; - QTextStream str(&string); - if (e.numTracks > 10 && e.numTracks <= 500) { - str << constNumTracksKey << constKeyValSep << e.numTracks << '\n'; - } - if (e.ratingFrom!=0 || e.ratingTo!=0) { - str << constRatingKey << constKeyValSep << e.ratingFrom << constRangeSep << e.ratingTo << '\n'; - } - if (e.minDuration!=0 || e.maxDuration!=0) { - str << constDurationKey << constKeyValSep << e.minDuration << constRangeSep << e.maxDuration << '\n'; - } - foreach (const Rule &rule, e.rules) { - if (!rule.isEmpty()) { - str << constRuleKey << '\n'; - Rule::ConstIterator it(rule.constBegin()); - Rule::ConstIterator end(rule.constEnd()); - for (; it!=end; ++it) { - str << it.key() << constKeyValSep << it.value() << '\n'; - } - } - } - - if (isRemote()) { - if (sendCommand(Save, QStringList() << e.name << string)) { - currentSave=e; - return true; - } - return false; - } - - QFile f(Utils::dataDir(constDir, true)+e.name+constExtension); - if (f.open(QIODevice::WriteOnly|QIODevice::Text)) { - QTextStream out(&f); - out.setCodec("UTF-8"); - out << string; - updateEntry(e); + if (sendCommand(Save, QStringList() << e.name << string)) { + currentSave=e; return true; } return false; } -void DynamicPlaylists::updateEntry(const Entry &e) -{ - QList::Iterator it=find(e.name); - if (it!=entryList.end()) { - entryList.replace(it-entryList.begin(), e); - QModelIndex idx=index(it-entryList.begin(), 0, QModelIndex()); - emit dataChanged(idx, idx); - } else { - beginInsertRows(QModelIndex(), entryList.count(), entryList.count()); - entryList.append(e); - endInsertRows(); - } -} - void DynamicPlaylists::del(const QString &name) { if (isRemote()) { if (sendCommand(Del, QStringList() << name)) { currentDelete=name; } - return; - } - - QList::Iterator it=find(name); - if (it==entryList.end()) { - return; - } - QString fName(Utils::dataDir(constDir, false)+name+constExtension); - bool isCurrent=currentEntry==name; - - if (!QFile::exists(fName) || QFile::remove(fName)) { - if (isCurrent) { - stop(); - } - beginRemoveRows(QModelIndex(), it-entryList.begin(), it-entryList.begin()); - entryList.erase(it); - endRemoveRows(); - return; + } else { + RulesPlaylists::del(name); } } @@ -373,14 +215,14 @@ void DynamicPlaylists::start(const QString &name) return; } - QString fName(Utils::dataDir(constDir, false)+name+constExtension); + QString fName(Utils::dataDir(rulesDir, false)+name+constExtension); if (!QFile::exists(fName)) { emit error(tr("Failed to locate rules file - %1").arg(fName)); return; } - QString rules(Utils::cacheDir(constDir, true)+constActiveRules); + QString rules(Utils::cacheDir(rulesDir, true)+constActiveRules); QFile::remove(rules); if (QFile::exists(rules)) { @@ -505,7 +347,7 @@ void DynamicPlaylists::enableRemotePolling(bool e) int DynamicPlaylists::getPid() const { - QFile pidFile(Utils::cacheDir(constDir, false)+constLockFile); + QFile pidFile(Utils::cacheDir(rulesDir, false)+constLockFile); if (pidFile.open(QIODevice::ReadOnly|QIODevice::Text)) { QTextStream str(&pidFile); @@ -540,85 +382,6 @@ bool DynamicPlaylists::controlApp(bool isStart) return rv; } -QList::Iterator DynamicPlaylists::find(const QString &e) -{ - QList::Iterator it(entryList.begin()); - QList::Iterator end(entryList.end()); - - for (; it!=end; ++it) { - if ((*it).name==e) { - break; - } - } - return it; -} - -void DynamicPlaylists::loadLocal() -{ - beginResetModel(); - entryList.clear(); - currentEntry=QString(); - - // Load all current enttries... - QString dirName=Utils::dataDir(constDir); - QDir d(dirName); - if (d.exists()) { - QStringList rulesFiles=d.entryList(QStringList() << QChar('*')+constExtension); - foreach (const QString &rf, rulesFiles) { - QFile f(dirName+rf); - if (f.open(QIODevice::ReadOnly|QIODevice::Text)) { - QStringList keys=QStringList() << constArtistKey << constSimilarArtistsKey << constAlbumArtistKey << constDateKey - << constExactKey << constAlbumKey << constTitleKey << constGenreKey << constFileKey << constExcludeKey; - - Entry e; - e.name=rf.left(rf.length()-constExtension.length()); - Rule r; - QTextStream in(&f); - in.setCodec("UTF-8"); - QStringList lines = in.readAll().split('\n', QString::SkipEmptyParts); - foreach (const QString &line, lines) { - QString str=line.trimmed(); - - if (str.isEmpty() || str.startsWith('#')) { - continue; - } - - if (str==constRuleKey) { - if (!r.isEmpty()) { - e.rules.append(r); - r.clear(); - } - } else if (str.startsWith(constRatingKey+constKeyValSep)) { - QStringList vals=str.mid(constRatingKey.length()+1).split(constRangeSep); - if (2==vals.count()) { - e.ratingFrom=vals.at(0).toUInt(); - e.ratingTo=vals.at(1).toUInt(); - } - } else if (str.startsWith(constDurationKey+constKeyValSep)) { - QStringList vals=str.mid(constDurationKey.length()+1).split(constRangeSep); - if (2==vals.count()) { - e.minDuration=vals.at(0).toUInt(); - e.maxDuration=vals.at(1).toUInt(); - } - } else { - foreach (const QString &k, keys) { - if (str.startsWith(k+constKeyValSep)) { - r.insert(k, str.mid(k.length()+1)); - } - } - } - } - if (!r.isEmpty()) { - e.rules.append(r); - r.clear(); - } - entryList.append(e); - } - } - } - endResetModel(); -} - void DynamicPlaylists::parseRemote(const QStringList &response) { DBUG << response; @@ -806,7 +569,7 @@ void DynamicPlaylists::checkHelper() } } else { // No timer => app startup! // Attempt to read current name... - QFileInfo inf(Utils::cacheDir(constDir, false)+constActiveRules); + QFileInfo inf(Utils::cacheDir(rulesDir, false)+constActiveRules); if (inf.exists() && inf.isSymLink()) { QString link=inf.readLink(); diff --git a/playlists/dynamicplaylists.h b/playlists/dynamicplaylists.h index b281d3424..4705ef912 100644 --- a/playlists/dynamicplaylists.h +++ b/playlists/dynamicplaylists.h @@ -29,13 +29,14 @@ #include #include #include +#include "rulesplaylists.h" #include "models/actionmodel.h" #include "support/icon.h" class QTimer; class NetworkJob; -class DynamicPlaylists : public ActionModel +class DynamicPlaylists : public RulesPlaylists { Q_OBJECT @@ -55,67 +56,24 @@ public: static QString toString(Command cmd); static void enableDebug(); - typedef QMap Rule; - struct Entry { - Entry(const QString &n=QString()) : name(n), ratingFrom(0), ratingTo(0), minDuration(0), maxDuration(0), numTracks(10) { } - bool operator==(const Entry &o) const { return name==o.name; } - bool haveRating() const { return ratingFrom>=0 && ratingTo>0; } - QString name; - QList rules; - int ratingFrom; - int ratingTo; - int minDuration; - int maxDuration; - int numTracks; - }; - static DynamicPlaylists * self(); - static const QString constRuleKey; - static const QString constArtistKey; - static const QString constSimilarArtistsKey; - static const QString constAlbumArtistKey; - static const QString constComposerKey; - static const QString constCommentKey; - static const QString constAlbumKey; - static const QString constTitleKey; - static const QString constGenreKey; - static const QString constDateKey; - static const QString constRatingKey; - static const QString constDurationKey; - static const QString constNumTracksKey; - static const QString constFileKey; - static const QString constExactKey; - static const QString constExcludeKey; - static const QChar constRangeSep; - DynamicPlaylists(); virtual ~DynamicPlaylists() { } QString name() const; QString title() const; QString descr() const; + bool isDynamic() const { return true; } + QVariant data(const QModelIndex &index, int role) const; const Icon & icon() const { return icn; } bool isRemote() const { return usingRemote; } - QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; - int rowCount(const QModelIndex &parent = QModelIndex()) const; - int columnCount(const QModelIndex&) const { return 1; } - bool hasChildren(const QModelIndex &parent) const; - QModelIndex parent(const QModelIndex &index) const; - QModelIndex index(int row, int column, const QModelIndex &parent) const; - QVariant data(const QModelIndex &, int) const; - Qt::ItemFlags flags(const QModelIndex &index) const; - Entry entry(const QString &e); - Entry entry(int row) const { return row>=0 && row & entries() const { return entryList; } void helperMessage(const QString &message) { Q_UNUSED(message) checkHelper(); } Action * startAct() const { return startAction; } Action * stopAct() const { return stopAction; } @@ -146,17 +104,11 @@ private: void pollRemoteHelper(); int getPid() const; bool controlApp(bool isStart); - QList::Iterator find(const QString &e); bool sendCommand(Command cmd, const QStringList &args=QStringList()); - void loadLocal(); void parseRemote(const QStringList &response); - void updateEntry(const Entry &e); private: - Icon icn; QTimer *localTimer; - QList entryList; - QString currentEntry; Action *startAction; Action *stopAction; diff --git a/playlists/dynamicplaylistspage.cpp b/playlists/dynamicplaylistspage.cpp index 060d1dda3..0e19cadfb 100644 --- a/playlists/dynamicplaylistspage.cpp +++ b/playlists/dynamicplaylistspage.cpp @@ -112,7 +112,7 @@ void DynamicPlaylistsPage::doSearch() void DynamicPlaylistsPage::controlActions() { - QModelIndexList selected=qobject_cast(sender()) ? QModelIndexList() : view->selectedIndexes(false); // Dont need sorted selection here... + QModelIndexList selected=qobject_cast(sender()) ? QModelIndexList() : view->selectedIndexes(false); // Dont need sorted selection here... editAction->setEnabled(1==selected.count()); DynamicPlaylists::self()->startAct()->setEnabled(1==selected.count()); @@ -130,7 +130,7 @@ void DynamicPlaylistsPage::remoteDynamicSupport(bool s) void DynamicPlaylistsPage::add() { - PlaylistRulesDialog *dlg=new PlaylistRulesDialog(this); + PlaylistRulesDialog *dlg=new PlaylistRulesDialog(this, DynamicPlaylists::self()); dlg->edit(QString()); } @@ -142,7 +142,7 @@ void DynamicPlaylistsPage::edit() return; } - PlaylistRulesDialog *dlg=new PlaylistRulesDialog(this); + PlaylistRulesDialog *dlg=new PlaylistRulesDialog(this, DynamicPlaylists::self()); dlg->edit(selected.at(0).data(Qt::DisplayRole).toString()); } diff --git a/playlists/dynamicplaylistspage.h b/playlists/dynamicplaylistspage.h index a4e782f9f..27164765c 100644 --- a/playlists/dynamicplaylistspage.h +++ b/playlists/dynamicplaylistspage.h @@ -21,8 +21,8 @@ * Boston, MA 02110-1301, USA. */ -#ifndef DYNAMICPAGE_H -#define DYNAMICPAGE_H +#ifndef DYNAMIC_RULES_PAGE_H +#define DYNAMIC_RULES_PAGE_H #include "widgets/singlepagewidget.h" #include "playlistproxymodel.h" diff --git a/playlists/playlistproxymodel.cpp b/playlists/playlistproxymodel.cpp index e6145c53d..b17678a48 100644 --- a/playlists/playlistproxymodel.cpp +++ b/playlists/playlistproxymodel.cpp @@ -45,13 +45,17 @@ bool PlaylistProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sour return true; } - DynamicPlaylists::Entry item = DynamicPlaylists::self()->entry(sourceRow); - foreach (const DynamicPlaylists::Rule & r, item.rules) { - DynamicPlaylists::Rule::ConstIterator it=r.constBegin(); - DynamicPlaylists::Rule::ConstIterator end=r.constEnd(); - for (; it!=end; ++it) { - if (matchesFilter(QStringList() << it.value())) { - return true; + RulesPlaylists *rules = qobject_cast(sourceModel()); + + if (rules) { + RulesPlaylists::Entry item = rules->entry(sourceRow); + foreach (const RulesPlaylists::Rule & r, item.rules) { + RulesPlaylists::Rule::ConstIterator it=r.constBegin(); + RulesPlaylists::Rule::ConstIterator end=r.constEnd(); + for (; it!=end; ++it) { + if (matchesFilter(QStringList() << it.value())) { + return true; + } } } } diff --git a/playlists/playlistrule.ui b/playlists/playlistrule.ui index f0d48f43e..9b8368dea 100644 --- a/playlists/playlistrule.ui +++ b/playlists/playlistrule.ui @@ -58,7 +58,7 @@ - + Artists similar to: diff --git a/playlists/playlistruledialog.cpp b/playlists/playlistruledialog.cpp index 8853a0a58..0d9a297bb 100644 --- a/playlists/playlistruledialog.cpp +++ b/playlists/playlistruledialog.cpp @@ -28,7 +28,12 @@ static const int constMinDate=1800; static const int constMaxDate=2100; -PlaylistRuleDialog::PlaylistRuleDialog(QWidget *parent) +#define REMOVE(w) \ + w->setVisible(false); \ + w->deleteLater(); \ + w=0; + +PlaylistRuleDialog::PlaylistRuleDialog(QWidget *parent, bool isDynamic) : Dialog(parent) , addingRules(false) { @@ -37,12 +42,17 @@ PlaylistRuleDialog::PlaylistRuleDialog(QWidget *parent) setMainWidget(mainWidet); setButtons(Ok|Cancel); enableButton(Ok, false); - setCaption(tr("Dynamic Rule")); + setCaption(isDynamic ? tr("Dynamic Rule") : tr("Smart Rule")); connect(artistText, SIGNAL(textChanged(const QString &)), SLOT(enableOkButton())); connect(composerText, SIGNAL(textChanged(const QString &)), SLOT(enableOkButton())); connect(commentText, SIGNAL(textChanged(const QString &)), SLOT(enableOkButton())); - connect(similarArtistsText, SIGNAL(textChanged(const QString &)), SLOT(enableOkButton())); + if (isDynamic) { + connect(similarArtistsText, SIGNAL(textChanged(const QString &)), SLOT(enableOkButton())); + } else { + REMOVE(similarArtistsText) + REMOVE(similarArtistsTextLabel) + } connect(albumArtistText, SIGNAL(textChanged(const QString &)), SLOT(enableOkButton())); connect(albumText, SIGNAL(textChanged(const QString &)), SLOT(enableOkButton())); connect(titleText, SIGNAL(textChanged(const QString &)), SLOT(enableOkButton())); @@ -64,8 +74,10 @@ PlaylistRuleDialog::PlaylistRuleDialog(QWidget *parent) artistText->clear(); artistText->insertItems(0, strings); - similarArtistsText->clear(); - similarArtistsText->insertItems(0, strings); + if (similarArtistsText) { + similarArtistsText->clear(); + similarArtistsText->insertItems(0, strings); + } strings=albumArtists.toList(); strings.sort(); @@ -109,25 +121,27 @@ PlaylistRuleDialog::~PlaylistRuleDialog() { } -bool PlaylistRuleDialog::edit(const DynamicPlaylists::Rule &rule, bool isAdd) +bool PlaylistRuleDialog::edit(const RulesPlaylists::Rule &rule, bool isAdd) { addingRules=isAdd; - typeCombo->setCurrentIndex(QLatin1String("true")==rule[DynamicPlaylists::constExcludeKey] ? 1 : 0); - artistText->setText(rule[DynamicPlaylists::constArtistKey]); - similarArtistsText->setText(rule[DynamicPlaylists::constSimilarArtistsKey]); - albumArtistText->setText(rule[DynamicPlaylists::constAlbumArtistKey]); - composerText->setText(rule[DynamicPlaylists::constComposerKey]); - commentText->setText(rule[DynamicPlaylists::constCommentKey]); - albumText->setText(rule[DynamicPlaylists::constAlbumKey]); - titleText->setText(rule[DynamicPlaylists::constTitleKey]); - genreText->setText(rule[DynamicPlaylists::constGenreKey]); - filenameText->setText(rule[DynamicPlaylists::constFileKey]); + typeCombo->setCurrentIndex(QLatin1String("true")==rule[RulesPlaylists::constExcludeKey] ? 1 : 0); + artistText->setText(rule[RulesPlaylists::constArtistKey]); + if (similarArtistsText) { + similarArtistsText->setText(rule[RulesPlaylists::constSimilarArtistsKey]); + } + albumArtistText->setText(rule[RulesPlaylists::constAlbumArtistKey]); + composerText->setText(rule[RulesPlaylists::constComposerKey]); + commentText->setText(rule[RulesPlaylists::constCommentKey]); + albumText->setText(rule[RulesPlaylists::constAlbumKey]); + titleText->setText(rule[RulesPlaylists::constTitleKey]); + genreText->setText(rule[RulesPlaylists::constGenreKey]); + filenameText->setText(rule[RulesPlaylists::constFileKey]); - QString date=rule[DynamicPlaylists::constDateKey]; + QString date=rule[RulesPlaylists::constDateKey]; int dateFrom=0; int dateTo=0; if (!date.isEmpty()) { - int idx=date.indexOf(DynamicPlaylists::constRangeSep); + int idx=date.indexOf(RulesPlaylists::constRangeSep); if (-1==idx) { dateFrom=date.toInt(); } else { @@ -144,7 +158,7 @@ bool PlaylistRuleDialog::edit(const DynamicPlaylists::Rule &rule, bool isAdd) } dateFromSpin->setValue(dateFrom); dateToSpin->setValue(dateTo); - exactCheck->setChecked(QLatin1String("false")!=rule[DynamicPlaylists::constExactKey]); + exactCheck->setChecked(QLatin1String("false")!=rule[RulesPlaylists::constExactKey]); errorLabel->setVisible(false); setButtons(isAdd ? User1|Ok|Close : Ok|Cancel); @@ -154,35 +168,35 @@ bool PlaylistRuleDialog::edit(const DynamicPlaylists::Rule &rule, bool isAdd) return QDialog::Accepted==exec(); } -DynamicPlaylists::Rule PlaylistRuleDialog::rule() const +RulesPlaylists::Rule PlaylistRuleDialog::rule() const { - DynamicPlaylists::Rule r; + RulesPlaylists::Rule r; if (!artist().isEmpty()) { - r.insert(DynamicPlaylists::constArtistKey, artist()); + r.insert(RulesPlaylists::constArtistKey, artist()); } if (!similarArtists().isEmpty()) { - r.insert(DynamicPlaylists::constSimilarArtistsKey, similarArtists()); + r.insert(RulesPlaylists::constSimilarArtistsKey, similarArtists()); } if (!albumArtist().isEmpty()) { - r.insert(DynamicPlaylists::constAlbumArtistKey, albumArtist()); + r.insert(RulesPlaylists::constAlbumArtistKey, albumArtist()); } if (!composer().isEmpty()) { - r.insert(DynamicPlaylists::constComposerKey, composer()); + r.insert(RulesPlaylists::constComposerKey, composer()); } if (!comment().isEmpty()) { - r.insert(DynamicPlaylists::constCommentKey, comment()); + r.insert(RulesPlaylists::constCommentKey, comment()); } if (!album().isEmpty()) { - r.insert(DynamicPlaylists::constAlbumKey, album()); + r.insert(RulesPlaylists::constAlbumKey, album()); } if (!title().isEmpty()) { - r.insert(DynamicPlaylists::constTitleKey, title()); + r.insert(RulesPlaylists::constTitleKey, title()); } if (!genre().isEmpty()) { - r.insert(DynamicPlaylists::constGenreKey, genre()); + r.insert(RulesPlaylists::constGenreKey, genre()); } if (!filename().isEmpty()) { - r.insert(DynamicPlaylists::constFileKey, filename()); + r.insert(RulesPlaylists::constFileKey, filename()); } int dateFrom=dateFromSpin->value(); int dateTo=dateToSpin->value(); @@ -190,18 +204,18 @@ DynamicPlaylists::Rule PlaylistRuleDialog::rule() const bool haveTo=dateTo>=constMinDate && dateTo<=constMaxDate && dateTo!=dateFrom; if (haveFrom && haveTo) { - r.insert(DynamicPlaylists::constDateKey, QString::number(dateFrom)+DynamicPlaylists::constRangeSep+QString::number(dateTo)); + r.insert(RulesPlaylists::constDateKey, QString::number(dateFrom)+RulesPlaylists::constRangeSep+QString::number(dateTo)); } else if (haveFrom) { - r.insert(DynamicPlaylists::constDateKey, QString::number(dateFrom)); + r.insert(RulesPlaylists::constDateKey, QString::number(dateFrom)); } else if (haveTo) { - r.insert(DynamicPlaylists::constDateKey, QString::number(dateTo)); + r.insert(RulesPlaylists::constDateKey, QString::number(dateTo)); } if (!exactCheck->isChecked()) { - r.insert(DynamicPlaylists::constExactKey, QLatin1String("false")); + r.insert(RulesPlaylists::constExactKey, QLatin1String("false")); } if (1==typeCombo->currentIndex()) { - r.insert(DynamicPlaylists::constExcludeKey, QLatin1String("true")); + r.insert(RulesPlaylists::constExcludeKey, QLatin1String("true")); } return r; } diff --git a/playlists/playlistruledialog.h b/playlists/playlistruledialog.h index e289cddd3..5f8a44bfd 100644 --- a/playlists/playlistruledialog.h +++ b/playlists/playlistruledialog.h @@ -34,15 +34,15 @@ class PlaylistRuleDialog : public Dialog, Ui::PlaylistRule Q_OBJECT public: - PlaylistRuleDialog(QWidget *parent); + PlaylistRuleDialog(QWidget *parent, bool isDynamic); virtual ~PlaylistRuleDialog(); - void createNew() { edit(DynamicPlaylists::Rule(), true); } - bool edit(const DynamicPlaylists::Rule &rule, bool isAdd=false); - DynamicPlaylists::Rule rule() const; + void createNew() { edit(RulesPlaylists::Rule(), true); } + bool edit(const RulesPlaylists::Rule &rule, bool isAdd=false); + RulesPlaylists::Rule rule() const; QString artist() const { return artistText->text().trimmed(); } - QString similarArtists() const { return similarArtistsText->text().trimmed(); } + QString similarArtists() const { return similarArtistsText ? similarArtistsText->text().trimmed() : QString(); } QString albumArtist() const { return albumArtistText->text().trimmed(); } QString composer() const { return composerText->text().trimmed(); } QString comment() const { return commentText->text().trimmed(); } @@ -52,7 +52,7 @@ public: QString filename() const { return filenameText->text().trimmed(); } Q_SIGNALS: - void addRule(const DynamicPlaylists::Rule &r); + void addRule(const RulesPlaylists::Rule &r); private Q_SLOTS: void enableOkButton(); diff --git a/playlists/playlistrules.ui b/playlists/playlistrules.ui index de7cf4e41..06521427e 100644 --- a/playlists/playlistrules.ui +++ b/playlists/playlistrules.ui @@ -196,7 +196,7 @@ - + Number of songs in play queue: diff --git a/playlists/playlistrulesdialog.cpp b/playlists/playlistrulesdialog.cpp index 06ab15d2a..f4f43a3d2 100644 --- a/playlists/playlistrulesdialog.cpp +++ b/playlists/playlistrulesdialog.cpp @@ -60,35 +60,35 @@ public: static QString translateStr(const QString &key) { - if (DynamicPlaylists::constArtistKey==key) { + if (RulesPlaylists::constArtistKey==key) { return QObject::tr("Artist"); - } else if (DynamicPlaylists::constSimilarArtistsKey==key) { + } else if (RulesPlaylists::constSimilarArtistsKey==key) { return QObject::tr("SimilarArtists"); - } else if (DynamicPlaylists::constAlbumArtistKey==key) { + } else if (RulesPlaylists::constAlbumArtistKey==key) { return QObject::tr("AlbumArtist"); - } else if (DynamicPlaylists::constComposerKey==key) { + } else if (RulesPlaylists::constComposerKey==key) { return QObject::tr("Composer"); - } else if (DynamicPlaylists::constCommentKey==key) { + } else if (RulesPlaylists::constCommentKey==key) { return QObject::tr("Comment"); - } else if (DynamicPlaylists::constAlbumKey==key) { + } else if (RulesPlaylists::constAlbumKey==key) { return QObject::tr("Album"); - } else if (DynamicPlaylists::constTitleKey==key) { + } else if (RulesPlaylists::constTitleKey==key) { return QObject::tr("Title"); - } else if (DynamicPlaylists::constGenreKey==key) { + } else if (RulesPlaylists::constGenreKey==key) { return QObject::tr("Genre"); - } else if (DynamicPlaylists::constDateKey==key) { + } else if (RulesPlaylists::constDateKey==key) { return QObject::tr("Date"); - } else if (DynamicPlaylists::constFileKey==key) { + } else if (RulesPlaylists::constFileKey==key) { return QObject::tr("File"); } else { return key; } } -static void update(QStandardItem *i, const DynamicPlaylists::Rule &rule) +static void update(QStandardItem *i, const RulesPlaylists::Rule &rule) { - DynamicPlaylists::Rule::ConstIterator it(rule.constBegin()); - DynamicPlaylists::Rule::ConstIterator end(rule.constEnd()); + RulesPlaylists::Rule::ConstIterator it(rule.constBegin()); + RulesPlaylists::Rule::ConstIterator end(rule.constEnd()); QMap v; QString str; QString type=QObject::tr("Include"); @@ -96,12 +96,12 @@ static void update(QStandardItem *i, const DynamicPlaylists::Rule &rule) bool include=true; for (int count=0; it!=end; ++it, ++count) { - if (DynamicPlaylists::constExcludeKey==it.key()) { + if (RulesPlaylists::constExcludeKey==it.key()) { if (QLatin1String("true")==it.value()) { type=QObject::tr("Exclude"); include=false; } - } else if (DynamicPlaylists::constExactKey==it.key()) { + } else if (RulesPlaylists::constExactKey==it.key()) { if (QLatin1String("false")==it.value()) { exact=false; } @@ -129,8 +129,9 @@ static void update(QStandardItem *i, const DynamicPlaylists::Rule &rule) i->setFlags(Qt::ItemIsSelectable| Qt::ItemIsEnabled); } -PlaylistRulesDialog::PlaylistRulesDialog(QWidget *parent) +PlaylistRulesDialog::PlaylistRulesDialog(QWidget *parent, RulesPlaylists *m) : Dialog(parent, "PlaylistRulesDialog") + , rules(m) , dlg(0) { QWidget *mainWidet = new QWidget(this); @@ -138,7 +139,7 @@ PlaylistRulesDialog::PlaylistRulesDialog(QWidget *parent) setMainWidget(mainWidet); setButtons(Ok|Cancel); enableButton(Ok, false); - setCaption(tr("Dynamic Rules")); + setCaption(rules->isDynamic() ? tr("Dynamic Rules") : tr("Smart Rules")); setAttribute(Qt::WA_DeleteOnClose); connect(addBtn, SIGNAL(clicked()), SLOT(add())); connect(editBtn, SIGNAL(clicked()), SLOT(edit())); @@ -146,7 +147,9 @@ PlaylistRulesDialog::PlaylistRulesDialog(QWidget *parent) connect(rulesList, SIGNAL(itemsSelected(bool)), SLOT(controlButtons())); connect(nameText, SIGNAL(textChanged(const QString &)), SLOT(enableOkButton())); connect(aboutLabel, SIGNAL(leftClickedUrl()), this, SLOT(showAbout())); - connect(DynamicPlaylists::self(), SIGNAL(saved(bool)), SLOT(saved(bool))); + if (rules->isDynamic()) { + connect(rules, SIGNAL(saved(bool)), SLOT(saved(bool))); + } messageWidget->setVisible(false); model=new QStandardItemModel(this); @@ -159,12 +162,20 @@ PlaylistRulesDialog::PlaylistRulesDialog(QWidget *parent) minDuration->setSpecialValueText(tr("No Limit")); maxDuration->setSpecialValueText(tr("No Limit")); + numTracks->setMinimum(rules->minTracks()); + numTracks->setMaximum(rules->maxTracks()); + controlButtons(); resize(500, 240); + if (!rules->isDynamic()) { + nameText->setPlaceholderText(tr("Name of Smart Rules")); + numberOfSongsLabel->setText(tr("Number of songs")); + } + static bool registered=false; if (!registered) { - qRegisterMetaType("Dynamic::Rule"); + qRegisterMetaType("RulesPlaylists::Rule"); registered=true; } } @@ -175,12 +186,12 @@ PlaylistRulesDialog::~PlaylistRulesDialog() void PlaylistRulesDialog::edit(const QString &name) { - DynamicPlaylists::Entry e=DynamicPlaylists::self()->entry(name); + RulesPlaylists::Entry e=rules->entry(name); if (model->rowCount()) { model->removeRows(0, model->rowCount()); } nameText->setText(name); - foreach (const DynamicPlaylists::Rule &r, e.rules) { + foreach (const RulesPlaylists::Rule &r, e.rules) { QStandardItem *item = new QStandardItem(); ::update(item, r); model->setItem(model->rowCount(), 0, item); @@ -229,13 +240,13 @@ void PlaylistRulesDialog::controlButtons() void PlaylistRulesDialog::add() { if (!dlg) { - dlg=new PlaylistRuleDialog(this); - connect(dlg, SIGNAL(addRule(const DynamicPlaylists::Rule&)), SLOT(addRule(const DynamicPlaylists::Rule&))); + dlg=new PlaylistRuleDialog(this, rules->isDynamic()); + connect(dlg, SIGNAL(addRule(const RulesPlaylists::Rule&)), SLOT(addRule(const RulesPlaylists::Rule&))); } dlg->createNew(); } -void PlaylistRulesDialog::addRule(const DynamicPlaylists::Rule &rule) +void PlaylistRulesDialog::addRule(const RulesPlaylists::Rule &rule) { QStandardItem *item = new QStandardItem(); ::update(item, rule); @@ -255,12 +266,12 @@ void PlaylistRulesDialog::edit() return; } if (!dlg) { - dlg=new PlaylistRuleDialog(this); - connect(dlg, SIGNAL(addRule(const DynamicPlaylists::Rule&)), SLOT(addRule(const DynamicPlaylists::Rule&))); + dlg=new PlaylistRuleDialog(this, rules->isDynamic()); + connect(dlg, SIGNAL(addRule(const RulesPlaylists::Rule&)), SLOT(addRule(const RulesPlaylists::Rule&))); } QModelIndex index=proxy->mapToSource(items.at(0)); QStandardItem *item=model->itemFromIndex(index); - DynamicPlaylists::Rule rule; + RulesPlaylists::Rule rule; QMap v=item->data().toMap(); QMap::ConstIterator it(v.constBegin()); QMap::ConstIterator end(v.constEnd()); @@ -291,23 +302,41 @@ void PlaylistRulesDialog::remove() void PlaylistRulesDialog::showAbout() { - MessageBox::information(this, - #ifdef Q_OS_MAC - tr("About dynamic rules")+QLatin1String("

")+ - #endif - tr("

Cantata will query your library using all of the rules listed. " - "The list of Include rules will be used to build a set of songs that can be used. " - "The list of Exclude rules will be used to build a set of songs that cannot be used. " - "If there are no Include rules, Cantata will assume that all songs (bar those from Exclude) can be used.

" - "

e.g. to have Cantata look for 'Rock songs by Wibble OR songs by Various Artists', you would need the following: " - "

  • Include AlbumArtist=Wibble Genre=Rock
  • Include AlbumArtist=Various Artists
" - "To have Cantata look for 'Songs by Wibble but not from album Abc', you would need the following: " - "
  • Include AlbumArtist=Wibble
  • Exclude AlbumArtist=Wibble Album=Abc
" - "After the set of usable songs has been created, Cantata will randomly select songs to " - "keep the play queue filled with specified number of entries (10 by default). If a range of ratings has been specified, then " - "only songs with a rating within this range will be used. Likewise, if a duration has been set.

") - ); - + if (rules->isDynamic()) { + MessageBox::information(this, + #ifdef Q_OS_MAC + tr("About dynamic rules")+QLatin1String("

")+ + #endif + tr("

Cantata will query your library using all of the rules listed. " + "The list of Include rules will be used to build a set of songs that can be used. " + "The list of Exclude rules will be used to build a set of songs that cannot be used. " + "If there are no Include rules, Cantata will assume that all songs (bar those from Exclude) can be used.

" + "

e.g. to have Cantata look for 'Rock songs by Wibble OR songs by Various Artists', you would need the following: " + "

  • Include AlbumArtist=Wibble Genre=Rock
  • Include AlbumArtist=Various Artists
" + "To have Cantata look for 'Songs by Wibble but not from album Abc', you would need the following: " + "
  • Include AlbumArtist=Wibble
  • Exclude AlbumArtist=Wibble Album=Abc
" + "After the set of usable songs has been created, Cantata will randomly select songs to " + "keep the play queue filled with specified number of entries (10 by default). If a range of ratings has been specified, then " + "only songs with a rating within this range will be used. Likewise, if a duration has been set.

") + ); + } else { + MessageBox::information(this, + #ifdef Q_OS_MAC + tr("About smart rules")+QLatin1String("

")+ + #endif + tr("

Cantata will query your library using all of the rules listed. " + "The list of Include rules will be used to build a set of songs that can be used. " + "The list of Exclude rules will be used to build a set of songs that cannot be used. " + "If there are no Include rules, Cantata will assume that all songs (bar those from Exclude) can be used.

" + "

e.g. to have Cantata look for 'Rock songs by Wibble OR songs by Various Artists', you would need the following: " + "

  • Include AlbumArtist=Wibble Genre=Rock
  • Include AlbumArtist=Various Artists
" + "To have Cantata look for 'Songs by Wibble but not from album Abc', you would need the following: " + "
  • Include AlbumArtist=Wibble
  • Exclude AlbumArtist=Wibble Album=Abc
" + "After the set of usable songs has been created, Cantata will add the desired number of songs to " + "the play queue. If a range of ratings has been specified, then " + "only songs with a rating within this range will be used. Likewise, if a duration has been set.

") + ); + } } void PlaylistRulesDialog::saved(bool s) @@ -332,13 +361,13 @@ bool PlaylistRulesDialog::save() return false; } - if (name!=origName && DynamicPlaylists::self()->exists(name) && + if (name!=origName && rules->exists(name) && MessageBox::No==MessageBox::warningYesNo(this, tr("A set of rules named '%1' already exists!\n\nOverwrite?").arg(name), tr("Overwrite Rules"), StdGuiItem::overwrite(), StdGuiItem::cancel())) { return false; } - DynamicPlaylists::Entry entry; + RulesPlaylists::Entry entry; entry.name=name; int from=ratingFrom->value(); int to=ratingTo->value(); @@ -361,7 +390,7 @@ bool PlaylistRulesDialog::save() QMap v=itm->data().toMap(); QMap::ConstIterator it(v.constBegin()); QMap::ConstIterator end(v.constEnd()); - DynamicPlaylists::Rule rule; + RulesPlaylists::Rule rule; for (; it!=end; ++it) { rule.insert(it.key(), it.value().toString()); } @@ -369,9 +398,9 @@ bool PlaylistRulesDialog::save() } } - bool saved=DynamicPlaylists::self()->save(entry); + bool saved=rules->save(entry); - if (DynamicPlaylists::self()->isRemote()) { + if (rules->isRemote()) { if (saved) { messageWidget->setInformation(tr("Saving %1").arg(name)); controls->setEnabled(false); @@ -380,7 +409,7 @@ bool PlaylistRulesDialog::save() return false; } else { if (saved && !origName.isEmpty() && entry.name!=origName) { - DynamicPlaylists::self()->del(origName); + rules->del(origName); } return saved; } diff --git a/playlists/playlistrulesdialog.h b/playlists/playlistrulesdialog.h index 6df098038..37cae079b 100644 --- a/playlists/playlistrulesdialog.h +++ b/playlists/playlistrulesdialog.h @@ -39,7 +39,7 @@ class PlaylistRulesDialog : public Dialog, Ui::PlaylistRules Q_OBJECT public: - PlaylistRulesDialog(QWidget *parent); + PlaylistRulesDialog(QWidget *parent, RulesPlaylists *m); virtual ~PlaylistRulesDialog(); void edit(const QString &name); @@ -54,12 +54,13 @@ private Q_SLOTS: void enableOkButton(); void controlButtons(); void add(); - void addRule(const DynamicPlaylists::Rule &rule); + void addRule(const RulesPlaylists::Rule &rule); void edit(); void remove(); void showAbout(); private: + RulesPlaylists *rules; RulesSort *proxy; QStandardItemModel *model; QString origName; diff --git a/playlists/playlistspage.cpp b/playlists/playlistspage.cpp index 4df7d5bb4..6cf9a4f67 100644 --- a/playlists/playlistspage.cpp +++ b/playlists/playlistspage.cpp @@ -26,6 +26,8 @@ #include "models/playlistsmodel.h" #include "dynamicplaylists.h" #include "dynamicplaylistspage.h" +#include "smartplaylists.h" +#include "smartplaylistspage.h" #include "storedplaylistspage.h" #include "gui/settings.h" @@ -36,7 +38,8 @@ PlaylistsPage::PlaylistsPage(QWidget *p) addPage(PlaylistsModel::self()->name(), PlaylistsModel::self()->icon(), PlaylistsModel::self()->title(), PlaylistsModel::self()->descr(), stored); dynamic=new DynamicPlaylistsPage(this); addPage(DynamicPlaylists::self()->name(), DynamicPlaylists::self()->icon(), DynamicPlaylists::self()->title(), DynamicPlaylists::self()->descr(), dynamic); - + smart=new SmartPlaylistsPage(this); + addPage(SmartPlaylists::self()->name(), SmartPlaylists::self()->icon(), SmartPlaylists::self()->title(), SmartPlaylists::self()->descr(), smart); connect(stored, SIGNAL(addToDevice(QString,QString,QList)), SIGNAL(addToDevice(QString,QString,QList))); Configuration config(metaObject()->className()); load(config); diff --git a/playlists/playlistspage.h b/playlists/playlistspage.h index db31037a3..2007f299e 100644 --- a/playlists/playlistspage.h +++ b/playlists/playlistspage.h @@ -29,6 +29,7 @@ class Action; class StoredPlaylistsPage; class DynamicPlaylistsPage; +class SmartPlaylistsPage; class PlaylistsPage : public MultiPageWidget { @@ -47,6 +48,7 @@ Q_SIGNALS: private: StoredPlaylistsPage *stored; DynamicPlaylistsPage *dynamic; + SmartPlaylistsPage *smart; }; #endif diff --git a/playlists/rulesplaylists.cpp b/playlists/rulesplaylists.cpp new file mode 100644 index 000000000..a2e899d2d --- /dev/null +++ b/playlists/rulesplaylists.cpp @@ -0,0 +1,306 @@ +/* + * Cantata + * + * Copyright (c) 2011-2017 Craig Drummond + * + * ---- + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "dynamicplaylists.h" +#include "config.h" +#include "support/utils.h" +#include "widgets/icons.h" +#include "models/roles.h" +#include "gui/settings.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +const QString RulesPlaylists::constExtension=QLatin1String(".rules"); +const QString RulesPlaylists::constRuleKey=QLatin1String("Rule"); +const QString RulesPlaylists::constArtistKey=QLatin1String("Artist"); +const QString RulesPlaylists::constSimilarArtistsKey=QLatin1String("SimilarArtists"); +const QString RulesPlaylists::constAlbumArtistKey=QLatin1String("AlbumArtist"); +const QString RulesPlaylists::constComposerKey=QLatin1String("Composer"); +const QString RulesPlaylists::constCommentKey=QLatin1String("Comment"); +const QString RulesPlaylists::constAlbumKey=QLatin1String("Album"); +const QString RulesPlaylists::constTitleKey=QLatin1String("Title"); +const QString RulesPlaylists::constGenreKey=QLatin1String("Genre"); +const QString RulesPlaylists::constDateKey=QLatin1String("Date"); +const QString RulesPlaylists::constRatingKey=QLatin1String("Rating"); +const QString RulesPlaylists::constDurationKey=QLatin1String("Duration"); +const QString RulesPlaylists::constNumTracksKey=QLatin1String("NumTracks"); +const QString RulesPlaylists::constFileKey=QLatin1String("File"); +const QString RulesPlaylists::constExactKey=QLatin1String("Exact"); +const QString RulesPlaylists::constExcludeKey=QLatin1String("Exclude"); +const QChar RulesPlaylists::constRangeSep=QLatin1Char('-'); +const QChar RulesPlaylists::constKeyValSep=QLatin1Char(':'); + +RulesPlaylists::RulesPlaylists(const QString &iconFile, const QString &dir) + : rulesDir(dir) +{ + icn.addFile(":"+iconFile+".svg"); + loadLocal(); +} + +QVariant RulesPlaylists::headerData(int, Qt::Orientation, int) const +{ + return QVariant(); +} + +int RulesPlaylists::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : entryList.count(); +} + +bool RulesPlaylists::hasChildren(const QModelIndex &parent) const +{ + return !parent.isValid(); +} + +QModelIndex RulesPlaylists::parent(const QModelIndex &) const +{ + return QModelIndex(); +} + +QModelIndex RulesPlaylists::index(int row, int column, const QModelIndex &parent) const +{ + if (parent.isValid() || !hasIndex(row, column, parent) || row>=entryList.count()) { + return QModelIndex(); + } + + return createIndex(row, column); +} + +QVariant RulesPlaylists::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + switch (role) { + case Cantata::Role_TitleText: + return title(); + case Cantata::Role_SubText: + return descr(); + case Qt::DecorationRole: + return icon(); + } + return QVariant(); + } + + if (index.parent().isValid() || index.row()>=entryList.count()) { + return QVariant(); + } + + switch (role) { + case Qt::ToolTipRole: + if (!Settings::self()->infoTooltips()) { + return QVariant(); + } + case Qt::DisplayRole: + return entryList.at(index.row()).name; + case Cantata::Role_SubText: { + const Entry &e=entryList.at(index.row()); + return tr("%n Rule(s)", "", e.rules.count())+(e.haveRating() ? tr(" - Rating: %1..%2") + .arg((double)e.ratingFrom/Song::Rating_Step).arg((double)e.ratingTo/Song::Rating_Step) : QString()); + } + default: + return QVariant(); + } +} + +Qt::ItemFlags RulesPlaylists::flags(const QModelIndex &index) const +{ + if (index.isValid()) { + return Qt::ItemIsSelectable | Qt::ItemIsEnabled; + } + return Qt::NoItemFlags; +} + +RulesPlaylists::Entry RulesPlaylists::entry(const QString &e) +{ + if (!e.isEmpty()) { + QList::Iterator it=find(e); + if (it!=entryList.end()) { + return *it; + } + } + + return Entry(); +} + +bool RulesPlaylists::save(const Entry &e) +{ + if (e.name.isEmpty()) { + return false; + } + + QString string; + QTextStream str(&string); + if (e.numTracks >= minTracks() && e.numTracks <= maxTracks()) { + str << constNumTracksKey << constKeyValSep << e.numTracks << '\n'; + } + if (e.ratingFrom!=0 || e.ratingTo!=0) { + str << constRatingKey << constKeyValSep << e.ratingFrom << constRangeSep << e.ratingTo << '\n'; + } + if (e.minDuration!=0 || e.maxDuration!=0) { + str << constDurationKey << constKeyValSep << e.minDuration << constRangeSep << e.maxDuration << '\n'; + } + foreach (const Rule &rule, e.rules) { + if (!rule.isEmpty()) { + str << constRuleKey << '\n'; + Rule::ConstIterator it(rule.constBegin()); + Rule::ConstIterator end(rule.constEnd()); + for (; it!=end; ++it) { + str << it.key() << constKeyValSep << it.value() << '\n'; + } + } + } + + if (isRemote()) { + return saveRemote(string, e); + } + + QFile f(Utils::dataDir(rulesDir, true)+e.name+constExtension); + if (f.open(QIODevice::WriteOnly|QIODevice::Text)) { + QTextStream out(&f); + out.setCodec("UTF-8"); + out << string; + updateEntry(e); + return true; + } + return false; +} + +void RulesPlaylists::updateEntry(const Entry &e) +{ + QList::Iterator it=find(e.name); + if (it!=entryList.end()) { + entryList.replace(it-entryList.begin(), e); + QModelIndex idx=index(it-entryList.begin(), 0, QModelIndex()); + emit dataChanged(idx, idx); + } else { + beginInsertRows(QModelIndex(), entryList.count(), entryList.count()); + entryList.append(e); + endInsertRows(); + } +} + +void RulesPlaylists::del(const QString &name) +{ + QList::Iterator it=find(name); + if (it==entryList.end()) { + return; + } + QString fName(Utils::dataDir(rulesDir, false)+name+constExtension); + bool isCurrent=currentEntry==name; + + if (!QFile::exists(fName) || QFile::remove(fName)) { + if (isCurrent) { + stop(); + } + beginRemoveRows(QModelIndex(), it-entryList.begin(), it-entryList.begin()); + entryList.erase(it); + endRemoveRows(); + return; + } +} + +QList::Iterator RulesPlaylists::find(const QString &e) +{ + QList::Iterator it(entryList.begin()); + QList::Iterator end(entryList.end()); + + for (; it!=end; ++it) { + if ((*it).name==e) { + break; + } + } + return it; +} + +void RulesPlaylists::loadLocal() +{ + beginResetModel(); + entryList.clear(); + currentEntry=QString(); + + // Load all current enttries... + QString dirName=Utils::dataDir(rulesDir); + QDir d(dirName); + if (d.exists()) { + QStringList rulesFiles=d.entryList(QStringList() << QChar('*')+constExtension); + foreach (const QString &rf, rulesFiles) { + QFile f(dirName+rf); + if (f.open(QIODevice::ReadOnly|QIODevice::Text)) { + QStringList keys=QStringList() << constArtistKey << constSimilarArtistsKey << constAlbumArtistKey << constDateKey + << constExactKey << constAlbumKey << constTitleKey << constGenreKey << constFileKey << constExcludeKey; + + Entry e; + e.name=rf.left(rf.length()-constExtension.length()); + Rule r; + QTextStream in(&f); + in.setCodec("UTF-8"); + QStringList lines = in.readAll().split('\n', QString::SkipEmptyParts); + foreach (const QString &line, lines) { + QString str=line.trimmed(); + + if (str.isEmpty() || str.startsWith('#')) { + continue; + } + + if (str==constRuleKey) { + if (!r.isEmpty()) { + e.rules.append(r); + r.clear(); + } + } else if (str.startsWith(constRatingKey+constKeyValSep)) { + QStringList vals=str.mid(constRatingKey.length()+1).split(constRangeSep); + if (2==vals.count()) { + e.ratingFrom=vals.at(0).toUInt(); + e.ratingTo=vals.at(1).toUInt(); + } + } else if (str.startsWith(constDurationKey+constKeyValSep)) { + QStringList vals=str.mid(constDurationKey.length()+1).split(constRangeSep); + if (2==vals.count()) { + e.minDuration=vals.at(0).toUInt(); + e.maxDuration=vals.at(1).toUInt(); + } + } else { + foreach (const QString &k, keys) { + if (str.startsWith(k+constKeyValSep)) { + r.insert(k, str.mid(k.length()+1)); + } + } + } + } + if (!r.isEmpty()) { + e.rules.append(r); + r.clear(); + } + entryList.append(e); + } + } + } + endResetModel(); +} diff --git a/playlists/rulesplaylists.h b/playlists/rulesplaylists.h new file mode 100644 index 000000000..5184e0538 --- /dev/null +++ b/playlists/rulesplaylists.h @@ -0,0 +1,115 @@ +/* + * Cantata + * + * Copyright (c) 2011-2017 Craig Drummond + * + * ---- + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef RULES_PLAYLISTS_H +#define RULES_PLAYLISTS_H + +#include +#include +#include +#include +#include +#include "models/actionmodel.h" +#include "support/icon.h" + +class RulesPlaylists : public ActionModel +{ + Q_OBJECT + +public: + typedef QMap Rule; + struct Entry { + Entry(const QString &n=QString()) : name(n), ratingFrom(0), ratingTo(0), minDuration(0), maxDuration(0), numTracks(10) { } + bool operator==(const Entry &o) const { return name==o.name; } + bool haveRating() const { return ratingFrom>=0 && ratingTo>0; } + QString name; + QList rules; + int ratingFrom; + int ratingTo; + int minDuration; + int maxDuration; + int numTracks; + }; + + static const QString constExtension; + static const QString constRuleKey; + static const QString constArtistKey; + static const QString constSimilarArtistsKey; + static const QString constAlbumArtistKey; + static const QString constComposerKey; + static const QString constCommentKey; + static const QString constAlbumKey; + static const QString constTitleKey; + static const QString constGenreKey; + static const QString constDateKey; + static const QString constRatingKey; + static const QString constDurationKey; + static const QString constNumTracksKey; + static const QString constFileKey; + static const QString constExactKey; + static const QString constExcludeKey; + static const QChar constRangeSep; + static const QChar constKeyValSep; + + RulesPlaylists(const QString &iconFile, const QString &dir); + virtual ~RulesPlaylists() { } + + virtual QString name() const =0; + virtual QString title() const =0; + virtual QString descr() const =0; + virtual bool isDynamic() const { return false; } + const Icon & icon() const { return icn; } + virtual bool isRemote() const { return false; } + virtual int minTracks() const { return 10; } + virtual int maxTracks() const { return 500; } + virtual bool saveRemote(const QString &string, const Entry &e) { Q_UNUSED(string); Q_UNUSED(e); return false; } + virtual void stop(bool sendClear=false) { Q_UNUSED(sendClear) } + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex&) const { return 1; } + bool hasChildren(const QModelIndex &parent) const; + QModelIndex parent(const QModelIndex &index) const; + QModelIndex index(int row, int column, const QModelIndex &parent) const; + QVariant data(const QModelIndex &, int) const; + Qt::ItemFlags flags(const QModelIndex &index) const; + Entry entry(const QString &e); + Entry entry(int row) const { return row>=0 && row & entries() const { return entryList; } + +protected: + QList::Iterator find(const QString &e); + void loadLocal(); + void updateEntry(const Entry &e); + +protected: + Icon icn; + QString rulesDir; + QList entryList; + QString currentEntry; +}; + +#endif diff --git a/playlists/smartplaylists.cpp b/playlists/smartplaylists.cpp new file mode 100644 index 000000000..283b5c729 --- /dev/null +++ b/playlists/smartplaylists.cpp @@ -0,0 +1,70 @@ +/* + * Cantata + * + * Copyright (c) 2011-2017 Craig Drummond + * + * ---- + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "smartplaylists.h" +#include "support/monoicon.h" +#include "support/globalstatic.h" +#include "models/roles.h" + +GLOBAL_STATIC(SmartPlaylists, instance) + +SmartPlaylists::SmartPlaylists() + : RulesPlaylists("gradcap", "smart") +{ + playlistIcon=MonoIcon::icon(FontAwesome::graduationcap, Utils::monoIconColor()); +} + +QString SmartPlaylists::name() const +{ + return QLatin1String("smart"); +} + +QString SmartPlaylists::title() const +{ + return tr("Smart Playlists"); +} + +QString SmartPlaylists::descr() const +{ + return tr("Rules based playlists"); +} + +QVariant SmartPlaylists::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return RulesPlaylists::data(index, role); + } + + if (index.parent().isValid() || index.row()>=entryList.count()) { + return QVariant(); + } + + switch (role) { + case Qt::DecorationRole: + return playlistIcon; + case Cantata::Role_Actions: + return ActionModel::data(index, role); + default: + return RulesPlaylists::data(index, role); + } +} diff --git a/playlists/smartplaylists.h b/playlists/smartplaylists.h new file mode 100644 index 000000000..f17632779 --- /dev/null +++ b/playlists/smartplaylists.h @@ -0,0 +1,50 @@ +/* + * Cantata + * + * Copyright (c) 2011-2017 Craig Drummond + * + * ---- + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef SMART_PLAYLISTS_H +#define SMART_PLAYLISTS_H + +#include +#include "rulesplaylists.h" + +class SmartPlaylists : public RulesPlaylists +{ + Q_OBJECT + +public: + static SmartPlaylists * self(); + + SmartPlaylists(); + virtual ~SmartPlaylists() { } + + QString name() const; + QString title() const; + QString descr() const; + QVariant data(const QModelIndex &index, int role) const; + int maxTracks() const { return 10000; } + +private: + QIcon playlistIcon; +}; + +#endif diff --git a/playlists/smartplaylistspage.cpp b/playlists/smartplaylistspage.cpp new file mode 100644 index 000000000..5052e0177 --- /dev/null +++ b/playlists/smartplaylistspage.cpp @@ -0,0 +1,382 @@ +/* + * Cantata + * + * Copyright (c) 2011-2017 Craig Drummond + * + * ---- + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "smartplaylistspage.h" +#include "smartplaylists.h" +#include "playlistrulesdialog.h" +#include "widgets/icons.h" +#include "support/action.h" +#include "support/configuration.h" +#include "mpd-interface/mpdconnection.h" +#include "support/messagebox.h" +#include "gui/stdactions.h" +#include "models/mpdlibrarymodel.h" + +SmartPlaylistsPage::SmartPlaylistsPage(QWidget *p) + : SinglePageWidget(p) +{ + addAction = new Action(Icons::self()->addNewItemIcon, tr("Add"), this); + editAction = new Action(Icons::self()->editIcon, tr("Edit"), this); + removeAction = new Action(Icons::self()->removeIcon, tr("Remove"), this); + + ToolButton *addBtn=new ToolButton(this); + ToolButton *editBtn=new ToolButton(this); + ToolButton *removeBtn=new ToolButton(this); + + addBtn->setDefaultAction(addAction); + editBtn->setDefaultAction(editAction); + removeBtn->setDefaultAction(removeAction); + + connect(this, SIGNAL(search(QByteArray,QString)), MPDConnection::self(), SLOT(search(QByteArray,QString))); + connect(MPDConnection::self(), SIGNAL(searchResponse(QString,QList)), this, SLOT(searchResponse(QString,QList))); + connect(this, SIGNAL(getRating(QString)), MPDConnection::self(), SLOT(getRating(QString))); + connect(MPDConnection::self(), SIGNAL(rating(QString,quint8)), this, SLOT(rating(QString,quint8))); + connect(view, SIGNAL(itemsSelected(bool)), this, SLOT(controlActions())); + connect(view, SIGNAL(headerClicked(int)), SLOT(headerClicked(int))); + connect(addAction, SIGNAL(triggered()), SLOT(addNew())); + connect(editAction, SIGNAL(triggered()), SLOT(edit())); + connect(removeAction, SIGNAL(triggered()), SLOT(remove())); + + proxy.setSourceModel(SmartPlaylists::self()); + view->setModel(&proxy); + view->setDeleteAction(removeAction); + view->setMode(ItemView::Mode_List); + controlActions(); + Configuration config(metaObject()->className()); + view->load(config); + controls=QList() << addBtn << editBtn << removeBtn; + init(ReplacePlayQueue|AppendToPlayQueue, QList(), controls); + + view->addAction(editAction); + view->addAction(removeAction); + view->alwaysShowHeader(); +} + +SmartPlaylistsPage::~SmartPlaylistsPage() +{ + Configuration config(metaObject()->className()); + view->save(config); +} + +void SmartPlaylistsPage::doSearch() +{ + QString text=view->searchText().trimmed(); + proxy.update(text); + if (proxy.enabled() && !proxy.filterText().isEmpty()) { + view->expandAll(); + } +} + +void SmartPlaylistsPage::controlActions() +{ + QModelIndexList selected=qobject_cast(sender()) ? QModelIndexList() : view->selectedIndexes(false); // Dont need sorted selection here... + StdActions::self()->enableAddToPlayQueue(1==selected.count()); + editAction->setEnabled(1==selected.count()); + removeAction->setEnabled(selected.count()); +} + +void SmartPlaylistsPage::addNew() +{ + PlaylistRulesDialog *dlg=new PlaylistRulesDialog(this, SmartPlaylists::self()); + dlg->edit(QString()); +} + +void SmartPlaylistsPage::edit() +{ + QModelIndexList selected=view->selectedIndexes(false); // Dont need sorted selection here... + + if (1!=selected.count()) { + return; + } + + PlaylistRulesDialog *dlg=new PlaylistRulesDialog(this, SmartPlaylists::self()); + dlg->edit(selected.at(0).data(Qt::DisplayRole).toString()); +} + +void SmartPlaylistsPage::remove() +{ + QModelIndexList selected=view->selectedIndexes(); + + if (selected.isEmpty() || + MessageBox::No==MessageBox::warningYesNo(this, tr("Are you sure you wish to remove the selected rules?\n\nThis cannot be undone."), + tr("Remove Smart Rules"), StdGuiItem::remove(), StdGuiItem::cancel())) { + return; + } + + QStringList names; + foreach (const QModelIndex &idx, selected) { + names.append(idx.data(Qt::DisplayRole).toString()); + } + + foreach (const QString &name, names) { + DynamicPlaylists::self()->del(name); + } +} + +void SmartPlaylistsPage::headerClicked(int level) +{ + if (0==level) { + emit close(); + } +} + +void SmartPlaylistsPage::enableWidgets(bool enable) +{ + foreach (QWidget *c, controls) { + c->setEnabled(enable); + } + + view->setEnabled(enable); +} + +void SmartPlaylistsPage::searchResponse(const QString &id, const QList &songs) +{ + if (id.length()<3 || id.mid(2).toInt()!=command.id || command.isEmpty()) { + return; + } + + if (id.startsWith("I:")) { + command.songs.unite(songs.toSet()); + } else if (id.startsWith("E:")) { + command.songs.subtract(songs.toSet()); + } + + if (command.includeRules.isEmpty()) { + if (command.songs.isEmpty()) { + command.clear(); + MessageBox::error(this, tr("Failed to locate any matching songs")); + return; + } + if (command.excludeRules.isEmpty()) { + filterCommand(); + } else { + emit search(command.excludeRules.takeFirst(), "E:"+QString::number(command.id)); + } + } else { + emit search(command.includeRules.takeFirst(), "I:"+QString::number(command.id)); + } +} + +void SmartPlaylistsPage::filterCommand() +{ + if (command.minDuration>0 || command.maxDuration>0) { + QSet toRemove; + for (const auto &s: command.songs) { + if (command.minDuration>s.time || (command.maxDuration>0 && s.time>command.maxDuration)) { + toRemove.insert(s); + } else { + command.toCheck.append(s.file); + } + } + command.songs.subtract(toRemove); + if (command.songs.isEmpty()) { + command.clear(); + MessageBox::error(this, tr("Failed to locate any matching songs")); + return; + } + } + + if (command.filterRating) { + if (command.toCheck.isEmpty()) { + for (const auto &s: command.songs) { + command.toCheck.append(s.file); + } + } + command.checking=command.toCheck.takeFirst(); + emit getRating(command.checking); + } else { + addSongsToPlayQueue(); + } +} + +void SmartPlaylistsPage::rating(const QString &file, quint8 val) +{ + if (command.isEmpty() || file!=command.checking) { + return; + } + + if (command.ratingFrom>val && command.ratingTo songs = command.songs.toList(); + command.songs.clear(); + std::random_shuffle(songs.begin(), songs.end()); + QStringList files; + for (int i=0; iclearSelection(); + } + command.clear(); +} + +void SmartPlaylistsPage::addSelectionToPlaylist(const QString &name, int action, quint8 priorty, bool decreasePriority) +{ + if (!name.isEmpty()) { + return; + } + + QModelIndexList selected=view->selectedIndexes(false); + if (1!=selected.count()) { + return; + } + + QModelIndex idx = proxy.mapToSource(selected.at(0)); + if (!idx.isValid()) { + return; + } + RulesPlaylists::Entry pl = SmartPlaylists::self()->entry(idx.row()); + if (pl.name.isEmpty() || pl.numTracks<=0) { + return; + } + + command = Command(pl.name, action, priorty, decreasePriority, command.id+1); + + QList::ConstIterator it = pl.rules.constBegin(); + QList::ConstIterator end = pl.rules.constEnd(); + QSet mpdGenres; + + for (; it!=end; ++it) { + QList dates; + QByteArray match = "find"; + bool isInclude = true; + RulesPlaylists::Rule::ConstIterator rIt = (*it).constBegin(); + RulesPlaylists::Rule::ConstIterator rEnd = (*it).constEnd(); + QByteArray baseRule; + QStringList genres; + + for (; rIt!=rEnd; ++rIt) { + if (RulesPlaylists::constDateKey==rIt.key()) { + QStringList parts=rIt.value().trimmed().split(RulesPlaylists::constRangeSep); + if (2==parts.length()) { + int from = parts.at(0).toInt(); + int to = parts.at(1).toInt(); + if (from > to) { + for (int i=to; i<=from; ++i) { + dates.append(i); + } + } else { + for (int i=from; i<=to; ++i) { + dates.append(i); + } + } + } else if (1==parts.length()) { + dates.append(parts.at(0).toInt()); + } + } else if (RulesPlaylists::constGenreKey==rIt.key() && rIt.value().trimmed().endsWith("*")) { + QString find=rIt.value().left(rIt.value().length()-1); + if (!find.isEmpty()) { + if (mpdGenres.isEmpty()) { + mpdGenres = MpdLibraryModel::self()->getGenres(); + } + foreach (const QString &g, mpdGenres) { + if (g.startsWith(find)) { + genres.append(g); + } + } + } + } else if (RulesPlaylists::constArtistKey==rIt.key() || RulesPlaylists::constAlbumKey==rIt.key() || + RulesPlaylists::constAlbumArtistKey==rIt.key() || RulesPlaylists::constComposerKey==rIt.key() || + RulesPlaylists::constCommentKey==rIt.key() || RulesPlaylists::constTitleKey==rIt.key() || + RulesPlaylists::constArtistKey==rIt.key() || RulesPlaylists::constGenreKey==rIt.key() || + RulesPlaylists::constFileKey==rIt.key()) { + baseRule += " " + rIt.key() + " " + MPDConnection::encodeName(rIt.value()); + } else if (RulesPlaylists::constExactKey==rIt.key()) { + if ("false" == rIt.value()) { + match = "search"; + } + } else if (RulesPlaylists::constExcludeKey==rIt.key()) { + if ("true" == rIt.value()) { + isInclude = false; + } + } + } + + if (!baseRule.isEmpty() || !genres.isEmpty() || !dates.isEmpty()) { + QList rules; + if (genres.isEmpty()) { + if (dates.isEmpty()) { + rules.append(match + baseRule); + } else { + foreach(int d, dates) { + rules.append(match + baseRule + " Date \"" + QByteArray::number(d) + "\""); + } + } + } else { + foreach (const QString &genre, genres) { + QByteArray rule = match + baseRule + " Genre " + MPDConnection::encodeName(genre); + if (dates.isEmpty()) { + rules.append(rule); + } else { + foreach(int d, dates) { + rules.append(rule + " Date \"" + QByteArray::number(d) + "\""); + } + } + } + } + if (!rules.isEmpty()) { + if (isInclude) { + command.includeRules += rules; + } else { + command.excludeRules += rules; + } + } + } + } + + command.filterRating = command.haveRating(); + if (command.includeRules.isEmpty()) { + if (command.haveRating()) { + command.includeRules.append("RATING:"+QByteArray::number(command.ratingFrom)+":"+QByteArray::number(command.ratingTo)); + command.filterRating = false; + } else { + command.includeRules.append(QByteArray()); + } + } + emit search(command.includeRules.takeFirst(), "I:"+QString::number(command.id)); +} diff --git a/playlists/smartplaylistspage.h b/playlists/smartplaylistspage.h new file mode 100644 index 000000000..c05603879 --- /dev/null +++ b/playlists/smartplaylistspage.h @@ -0,0 +1,103 @@ +/* + * Cantata + * + * Copyright (c) 2011-2017 Craig Drummond + * + * ---- + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef SMART_PLAYLISTS_PAGE_H +#define SMART_PLAYLISTS_PAGE_H + +#include "widgets/singlepagewidget.h" +#include "playlistproxymodel.h" +#include "rulesplaylists.h" + +class Action; +class QLabel; + +class SmartPlaylistsPage : public SinglePageWidget +{ + Q_OBJECT + + struct Command { + Command(const RulesPlaylists::Entry &e=RulesPlaylists::Entry(), int a=0, quint8 prio=0, bool dec=false, quint32 i=0) + : playlist(e.name), action(a), priorty(prio), decreasePriority(dec), ratingFrom(e.ratingFrom), ratingTo(e.ratingTo), + minDuration(e.minDuration), maxDuration(e.maxDuration), numTracks(e.numTracks), id(i) { } + bool isEmpty() const { return playlist.isEmpty(); } + void clear() { playlist.clear(); includeRules.clear(); excludeRules.clear(); songs.clear(); toCheck.clear(); checking.clear(); } + bool haveRating() const { return ratingFrom>=0 && ratingTo>0; } + + QString playlist; + + int action; + quint8 priorty; + bool decreasePriority; + + QList includeRules; + QList excludeRules; + + bool filterRating = false; + int ratingFrom = 0; + int ratingTo = 0; + int minDuration = 0; + int maxDuration = 0; + int numTracks = 0; + + quint32 id; + + QString checking; + QSet songs; + QStringList toCheck; + }; + +public: + SmartPlaylistsPage(QWidget *p); + virtual ~SmartPlaylistsPage(); + void setView(int) { } + +Q_SIGNALS: + void search(const QByteArray &query, const QString &id); + void getRating(const QString &file); + +private Q_SLOTS: + void addNew(); + void edit(); + void remove(); + void headerClicked(int level); + void searchResponse(const QString &id, const QList &songs); + void rating(const QString &file, quint8 val); + +private: + void doSearch(); + void controlActions(); + void enableWidgets(bool enable); + void filterCommand(); + void addSongsToPlayQueue(); + void addSelectionToPlaylist(const QString &name, int action, quint8 priorty, bool decreasePriority); + +private: + PlaylistProxyModel proxy; + Action *addAction; + Action *editAction; + Action *removeAction; + QList controls; + Command command; +}; + +#endif diff --git a/translations/blank.ts b/translations/blank.ts index 3b296e9c3..2eb99eaa6 100644 --- a/translations/blank.ts +++ b/translations/blank.ts @@ -1538,6 +1538,21 @@ This cannot be undone. + + RulesPlaylists + + + - Rating: %1..%2 + + + + + %n Rule(s) + + + + + DynamicPlaylists @@ -1560,18 +1575,6 @@ This cannot be undone. Dynamically generated playlists - - - - Rating: %1..%2 - - - - - %n Rule(s) - - - - You need to install "perl" on your system in order for Cantata's dynamic mode to function. diff --git a/translations/cantata_cs.ts b/translations/cantata_cs.ts index 4abedfda7..fb5816957 100644 --- a/translations/cantata_cs.ts +++ b/translations/cantata_cs.ts @@ -7839,6 +7839,23 @@ Tento krok nelze vrátit zpět. Pozastavit + + RulesPlaylists + + + - Rating: %1..%2 + - Hodnocení: %1...%2 + + + + %n Rule(s) + + Pravidla: %n + Pravidla: %n + Pravidla: %n + + + DynamicPlaylists @@ -7861,20 +7878,6 @@ Tento krok nelze vrátit zpět. Dynamically generated playlists Dynamicky tvořené seznamy skladeb - - - - Rating: %1..%2 - - Hodnocení: %1...%2 - - - - %n Rule(s) - - Pravidla: %n - Pravidla: %n - Pravidla: %n - - You need to install "perl" on your system in order for Cantata's dynamic mode to function. diff --git a/translations/cantata_de.ts b/translations/cantata_de.ts index a65d3fc2c..6e60b19ac 100644 --- a/translations/cantata_de.ts +++ b/translations/cantata_de.ts @@ -5419,6 +5419,22 @@ This cannot be undone. + + RulesPlaylists + + + - Rating: %1..%2 + + + + + %n Rule(s) + + Eine Regel + %n Regeln + + + DynamicPlaylists @@ -5441,19 +5457,6 @@ This cannot be undone. Dynamically generated playlists - - - - Rating: %1..%2 - - - - - %n Rule(s) - - Eine Regel - %n Regeln - - You need to install "perl" on your system in order for Cantata's dynamic mode to function. diff --git a/translations/cantata_en_GB.ts b/translations/cantata_en_GB.ts index fcc3976fb..88cd13ec7 100644 --- a/translations/cantata_en_GB.ts +++ b/translations/cantata_en_GB.ts @@ -1627,6 +1627,22 @@ This cannot be undone. + + RulesPlaylists + + + - Rating: %1..%2 + + + + + %n Rule(s) + + %n Rule + %n Rules + + + DynamicPlaylists @@ -1649,19 +1665,6 @@ This cannot be undone. Dynamically generated playlists - - - - Rating: %1..%2 - - - - - %n Rule(s) - - %n Rule - %n Rules - - You need to install "perl" on your system in order for Cantata's dynamic mode to function. diff --git a/translations/cantata_es.ts b/translations/cantata_es.ts index 4124539e5..5a080fee3 100644 --- a/translations/cantata_es.ts +++ b/translations/cantata_es.ts @@ -6416,6 +6416,22 @@ This cannot be undone. + + RulesPlaylists + + + - Rating: %1..%2 + + + + + %n Rule(s) + + 1 norma + %n normas + + + DynamicPlaylists @@ -6438,19 +6454,6 @@ This cannot be undone. Dynamically generated playlists - - - - Rating: %1..%2 - - - - - %n Rule(s) - - 1 norma - %n normas - - You need to install "perl" on your system in order for Cantata's dynamic mode to function. diff --git a/translations/cantata_fr.ts b/translations/cantata_fr.ts index 8f3444f08..71d5cf670 100644 --- a/translations/cantata_fr.ts +++ b/translations/cantata_fr.ts @@ -6747,6 +6747,22 @@ Cette action est définitive. Pause + + RulesPlaylists + + + - Rating: %1..%2 + - Note: %1..%2 + + + + %n Rule(s) + + 1 règle + %n règles + + + DynamicPlaylists @@ -6769,19 +6785,6 @@ Cette action est définitive. Dynamically generated playlists - - - - Rating: %1..%2 - - Note: %1..%2 - - - - %n Rule(s) - - 1 règle - %n règles - - You need to install "perl" on your system in order for Cantata's dynamic mode to function. diff --git a/translations/cantata_hu.ts b/translations/cantata_hu.ts index fcb819221..246b0c1ce 100644 --- a/translations/cantata_hu.ts +++ b/translations/cantata_hu.ts @@ -7243,6 +7243,21 @@ Ezt nem lehet visszavonni! Szünet + + RulesPlaylists + + + - Rating: %1..%2 + - Besorolás: %1..%2 + + + + %n Rule(s) + + %n Szabály + + + DynamicPlaylists @@ -7265,18 +7280,6 @@ Ezt nem lehet visszavonni! Dynamically generated playlists - - - - Rating: %1..%2 - - Besorolás: %1..%2 - - - - %n Rule(s) - - %n Szabály - - You need to install "perl" on your system in order for Cantata's dynamic mode to function. diff --git a/translations/cantata_it.ts b/translations/cantata_it.ts index def9c098c..db4fddccb 100644 --- a/translations/cantata_it.ts +++ b/translations/cantata_it.ts @@ -1559,6 +1559,22 @@ Non sarà possibile tornare indietro. Pausa + + RulesPlaylists + + + - Rating: %1..%2 + - Valutazione: %1..%2 + + + + %n Rule(s) + + %n Regola + %n Regole + + + DynamicPlaylists @@ -1581,19 +1597,6 @@ Non sarà possibile tornare indietro. Dynamically generated playlists Scalette generate dinamicamente - - - - Rating: %1..%2 - - Valutazione: %1..%2 - - - - %n Rule(s) - - %n Regola - %n Regole - - You need to install "perl" on your system in order for Cantata's dynamic mode to function. diff --git a/translations/cantata_ja.ts b/translations/cantata_ja.ts index 814bba226..f1c09f77e 100644 --- a/translations/cantata_ja.ts +++ b/translations/cantata_ja.ts @@ -7695,6 +7695,21 @@ This cannot be undone. 一時停止 + + RulesPlaylists + + + - Rating: %1..%2 + - レーティング: %1..%2 + + + + %n Rule(s) + + %n ルール + + + DynamicPlaylists @@ -7717,18 +7732,6 @@ This cannot be undone. Dynamically generated playlists 動的に生成されたプレイリスト - - - - Rating: %1..%2 - - レーティング: %1..%2 - - - - %n Rule(s) - - %n ルール - - You need to install "perl" on your system in order for Cantata's dynamic mode to function. diff --git a/translations/cantata_ko.ts b/translations/cantata_ko.ts index ac6b4178e..e6e3e0b70 100644 --- a/translations/cantata_ko.ts +++ b/translations/cantata_ko.ts @@ -7817,6 +7817,21 @@ This cannot be undone. 멈춤 + + RulesPlaylists + + + - Rating: %1..%2 + - 등급: %1..%2 + + + + %n Rule(s) + + %n 규정 + + + DynamicPlaylists @@ -7839,18 +7854,6 @@ This cannot be undone. Dynamically generated playlists 동적 생성 연주목록 - - - - Rating: %1..%2 - - 등급: %1..%2 - - - - %n Rule(s) - - %n 규정 - - You need to install "perl" on your system in order for Cantata's dynamic mode to function. diff --git a/translations/cantata_pl.ts b/translations/cantata_pl.ts index 2d437472b..0b734ec9d 100644 --- a/translations/cantata_pl.ts +++ b/translations/cantata_pl.ts @@ -7850,15 +7850,33 @@ Ta operacja nie może być cofnięta. DockMenu - - - Play - Odtwarzaj + + - Rating: %1..%2 + - Ocena: %1..%2 + + + %n Rule(s) + + 1 Reguła + %n Reguły + %n Reguł + + + + + RulesPlaylists - - Pause - Wstrzymaj + + - Rating: %1..%2 + + + + + %n Rule(s) + + + @@ -7883,20 +7901,6 @@ Ta operacja nie może być cofnięta. Dynamically generated playlists Dynamicznie generowane playlisty - - - - Rating: %1..%2 - - Ocena: %1..%2 - - - - %n Rule(s) - - 1 Reguła - %n Reguły - %n Reguł - - You need to install "perl" on your system in order for Cantata's dynamic mode to function. diff --git a/translations/cantata_ru.ts b/translations/cantata_ru.ts index 1235765bc..02b75cb5c 100644 --- a/translations/cantata_ru.ts +++ b/translations/cantata_ru.ts @@ -7265,6 +7265,23 @@ This cannot be undone. Пауза + + RulesPlaylists + + + - Rating: %1..%2 + - Рейтинг: %1..%2 + + + + %n Rule(s) + + подкаст: %n + подкаст: %n + подкаст: %n + + + DynamicPlaylists @@ -7287,20 +7304,6 @@ This cannot be undone. Dynamically generated playlists - - - - Rating: %1..%2 - - Рейтинг: %1..%2 - - - - %n Rule(s) - - подкаст: %n - подкаст: %n - подкаст: %n - - You need to install "perl" on your system in order for Cantata's dynamic mode to function. diff --git a/translations/cantata_zh_CN.ts b/translations/cantata_zh_CN.ts index a78cd9461..df0c57d81 100644 --- a/translations/cantata_zh_CN.ts +++ b/translations/cantata_zh_CN.ts @@ -3976,6 +3976,21 @@ This cannot be undone. + + RulesPlaylists + + + - Rating: %1..%2 + + + + + %n Rule(s) + + %n 规则 + + + DynamicPlaylists @@ -3998,18 +4013,6 @@ This cannot be undone. Dynamically generated playlists - - - - Rating: %1..%2 - - - - - %n Rule(s) - - %n 规则 - - You need to install "perl" on your system in order for Cantata's dynamic mode to function.