Files
cantata/playlists/smartplaylistspage.cpp

473 lines
16 KiB
C++

/*
* Cantata
*
* Copyright (c) 2011-2017 Craig Drummond <craig.p.drummond@gmail.com>
*
* ----
*
* 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<Song>)), this, SLOT(searchResponse(QString,QList<Song>)));
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<QWidget *>() << addBtn << editBtn << removeBtn;
init(ReplacePlayQueue|AppendToPlayQueue, QList<QWidget *>(), 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<RulesPlaylists *>(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<Song> &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<Song> 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 || command.fetchRatings) {
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;
}
for (auto &s: command.songs) {
if (s.file==file) {
s.rating=val;
if (command.filterRating && (val<command.ratingFrom || val>command.ratingTo)) {
command.songs.remove(s);
}
break;
}
}
if (command.toCheck.isEmpty()) {
command.checking.clear();
addSongsToPlayQueue();
} else {
command.checking=command.toCheck.takeFirst();
emit getRating(command.checking);
}
}
static bool sortAscending = true;
static bool composerSort(const Song &s1, const Song &s2)
{
const QString v1=s1.hasComposer() ? s1.composer() : QString();
const QString v2=s2.hasComposer() ? s2.composer() : QString();
int c=v1.localeAwareCompare(v2);
return sortAscending ? (c<0 || (c==0 && s1<s2)) : (c>0 || (c==0 && s1<s2));
}
static bool artistSort(const Song &s1, const Song &s2)
{
const QString v1=s1.hasArtistSort() ? s1.artistSort() : s1.artist;
const QString v2=s2.hasArtistSort() ? s2.artistSort() : s2.artist;
int c=v1.localeAwareCompare(v2);
return sortAscending ? (c<0 || (c==0 && s1<s2)) : (c>0 || (c==0 && s1<s2));
}
static bool albumArtistSort(const Song &s1, const Song &s2)
{
const QString v1=s1.hasAlbumArtistSort() ? s1.albumArtistSort() : s1.artistOrComposer();
const QString v2=s2.hasAlbumArtistSort() ? s2.albumArtistSort() : s2.artistOrComposer();
int c=v1.localeAwareCompare(v2);
return sortAscending ? (c<0 || (c==0 && s1<s2)) : (c>0 || (c==0 && s1<s2));
}
static bool albumSort(const Song &s1, const Song &s2)
{
const QString v1=s1.hasAlbumSort() ? s1.albumSort() : s1.album;
const QString v2=s2.hasAlbumSort() ? s2.albumSort() : s2.album;
int c=v1.localeAwareCompare(v2);
return sortAscending ? (c<0 || (c==0 && s1<s2)) : (c>0 || (c==0 && s1<s2));
}
static bool genreSort(const Song &s1, const Song &s2)
{
int c=s1.compareGenres(s2);
return sortAscending ? (c<0 || (c==0 && s1<s2)) : (c>0 || (c==0 && s1<s2));
}
static bool dateSort(const Song &s1, const Song &s2)
{
return sortAscending ? (s1.year<s2.year || (s1.year==s2.year && s1<s2)) : (s1.year>s2.year || (s1.year==s2.year && s1<s2));
}
static bool ratingSort(const Song &s1, const Song &s2)
{
return sortAscending ? (s1.rating<s2.rating || (s1.rating==s2.rating && s1<s2))
: (s1.rating>s2.rating || (s1.rating==s2.rating && s1<s2));
}
static bool ageSort(const Song &s1, const Song &s2)
{
return sortAscending ? (s1.lastModified<s2.lastModified || (s1.lastModified==s2.lastModified && s1<s2))
: (s1.lastModified>s2.lastModified || (s1.lastModified==s2.lastModified && s1<s2));
}
void SmartPlaylistsPage::addSongsToPlayQueue()
{
if (command.songs.isEmpty()) {
command.clear();
MessageBox::error(this, tr("Failed to locate any matching songs"));
return;
}
QList<Song> songs = command.songs.toList();
command.songs.clear();
sortAscending = command.orderAscending;
switch(command.order) {
case RulesPlaylists::Order_AlbumArtist:
qSort(songs.begin(), songs.end(), albumArtistSort);
break;
case RulesPlaylists::Order_Artist:
qSort(songs.begin(), songs.end(), artistSort);
break;
case RulesPlaylists::Order_Album:
qSort(songs.begin(), songs.end(), albumSort);
break;
case RulesPlaylists::Order_Composer:
qSort(songs.begin(), songs.end(), composerSort);
break;
case RulesPlaylists::Order_Date:
qSort(songs.begin(), songs.end(), dateSort);
break;
case RulesPlaylists::Order_Genre:
qSort(songs.begin(), songs.end(), genreSort);
break;
case RulesPlaylists::Order_Rating:
qSort(songs.begin(), songs.end(), ratingSort);
break;
case RulesPlaylists::Order_Age:
qSort(songs.begin(), songs.end(), ageSort);
break;
default:
case RulesPlaylists::Order_Random:
std::random_shuffle(songs.begin(), songs.end());
}
QStringList files;
for (int i=0; i<command.numTracks && !songs.isEmpty(); ++i) {
files.append(songs.takeFirst().file);
}
if (!files.isEmpty()) {
emit add(files, command.action, command.priorty, command.decreasePriority);
view->clearSelection();
}
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, action, priorty, decreasePriority, command.id+1);
QList<RulesPlaylists::Rule>::ConstIterator it = pl.rules.constBegin();
QList<RulesPlaylists::Rule>::ConstIterator end = pl.rules.constEnd();
QSet<QString> mpdGenres;
for (; it!=end; ++it) {
QList<int> 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<QByteArray> 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();
command.fetchRatings = RulesPlaylists::Order_Rating == command.order;
if (command.includeRules.isEmpty()) {
if (command.haveRating()) {
command.includeRules.append("RATING:"+QByteArray::number(command.ratingFrom)+":"+QByteArray::number(command.ratingTo));
command.filterRating = false;
command.fetchRatings = false;
} else {
command.includeRules.append(QByteArray());
}
}
emit search(command.includeRules.takeFirst(), "I:"+QString::number(command.id));
}