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 playlistsDynamicky 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 playlistsScalette 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 playlistsDynamicznie 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.