Use common filter parser for collection and playlist
This commit is contained in:
parent
dd904fe3c2
commit
6543e4c5da
|
@ -58,9 +58,11 @@ set(SOURCES
|
|||
utilities/filemanagerutils.cpp
|
||||
utilities/coverutils.cpp
|
||||
utilities/screenutils.cpp
|
||||
utilities/searchparserutils.cpp
|
||||
utilities/textencodingutils.cpp
|
||||
|
||||
filterparser/filterparser.cpp
|
||||
filterparser/filtertree.cpp
|
||||
|
||||
engine/enginebase.cpp
|
||||
engine/enginedevice.cpp
|
||||
engine/devicefinders.cpp
|
||||
|
@ -106,7 +108,6 @@ set(SOURCES
|
|||
playlist/playlistcontainer.cpp
|
||||
playlist/playlistdelegates.cpp
|
||||
playlist/playlistfilter.cpp
|
||||
playlist/playlistfilterparser.cpp
|
||||
playlist/playlistheader.cpp
|
||||
playlist/playlistitem.cpp
|
||||
playlist/playlistlistcontainer.cpp
|
||||
|
|
|
@ -19,31 +19,17 @@
|
|||
|
||||
#include "config.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "utilities/timeconstants.h"
|
||||
#include "utilities/searchparserutils.h"
|
||||
|
||||
#include "filterparser/filterparser.h"
|
||||
#include "filterparser/filtertree.h"
|
||||
#include "collectionfilter.h"
|
||||
#include "collectionmodel.h"
|
||||
#include "collectionitem.h"
|
||||
|
||||
const QStringList CollectionFilter::Operators = QStringList() << QStringLiteral(":")
|
||||
<< QStringLiteral("=")
|
||||
<< QStringLiteral("==")
|
||||
<< QStringLiteral("<>")
|
||||
<< QStringLiteral("<")
|
||||
<< QStringLiteral("<=")
|
||||
<< QStringLiteral(">")
|
||||
<< QStringLiteral(">=");
|
||||
|
||||
CollectionFilter::CollectionFilter(QObject *parent) : QSortFilterProxyModel(parent) {
|
||||
CollectionFilter::CollectionFilter(QObject *parent) : QSortFilterProxyModel(parent), query_hash_(0) {
|
||||
|
||||
setSortLocaleAware(true);
|
||||
setDynamicSortFilter(true);
|
||||
|
@ -60,274 +46,30 @@ bool CollectionFilter::filterAcceptsRow(const int source_row, const QModelIndex
|
|||
CollectionItem *item = model->IndexToItem(idx);
|
||||
if (!item) return false;
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
QString filter_string = filterRegularExpression().pattern().remove(QLatin1Char('\\'));
|
||||
#else
|
||||
QString filter_string = filterRegExp().pattern();
|
||||
#endif
|
||||
|
||||
if (filter_string.isEmpty()) return true;
|
||||
if (filter_string_.isEmpty()) return true;
|
||||
|
||||
if (item->type != CollectionItem::Type::Song) {
|
||||
return item->type == CollectionItem::Type::LoadingIndicator;
|
||||
}
|
||||
|
||||
for (const QString &foperator : Operators) {
|
||||
if (filter_string.contains(foperator + QLatin1Char(' '))) {
|
||||
filter_string = filter_string.replace(foperator + QLatin1Char(' '), foperator);
|
||||
}
|
||||
if (filter_string.contains(QLatin1Char(' ') + foperator)) {
|
||||
filter_string = filter_string.replace(QLatin1Char(' ') + foperator, foperator);
|
||||
}
|
||||
}
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
const QStringList tokens = filter_string.split(QLatin1Char(' '), Qt::SkipEmptyParts);
|
||||
#else
|
||||
const QStringList tokens = filter_string.split(QLatin1Char(' '), QString::SkipEmptyParts);
|
||||
#endif
|
||||
QStringList filter_strings;
|
||||
|
||||
FilterList filters;
|
||||
static QRegularExpression operator_regex(QStringLiteral("(=|<[>=]?|>=?|!=)"));
|
||||
for (int i = 0; i < tokens.count(); ++i) {
|
||||
const QString &token = tokens[i];
|
||||
if (token.contains(QLatin1Char(':'))) {
|
||||
QString field = token.section(QLatin1Char(':'), 0, 0).remove(QLatin1Char(':')).trimmed();
|
||||
QString value = token.section(QLatin1Char(':'), 1, -1).remove(QLatin1Char(':')).trimmed();
|
||||
if (field.isEmpty() || value.isEmpty()) continue;
|
||||
if (Song::kTextSearchColumns.contains(field, Qt::CaseInsensitive) && value.count(QLatin1Char('"')) <= 2) {
|
||||
bool quotation_mark_start = false;
|
||||
bool quotation_mark_end = false;
|
||||
if (value.left(1) == QLatin1Char('"')) {
|
||||
value.remove(0, 1);
|
||||
quotation_mark_start = true;
|
||||
if (value.length() >= 1 && value.count(QLatin1Char('"')) == 1) {
|
||||
value = value.section(QLatin1Char(QLatin1Char('"')), 0, 0).remove(QLatin1Char('"')).trimmed();
|
||||
quotation_mark_end = true;
|
||||
}
|
||||
}
|
||||
for (int y = i + 1; y < tokens.count() && !quotation_mark_end; ++y) {
|
||||
QString next_value = tokens[y];
|
||||
if (!quotation_mark_start && ContainsOperators(next_value)) {
|
||||
break;
|
||||
}
|
||||
if (quotation_mark_start && next_value.contains(QLatin1Char('"'))) {
|
||||
next_value = next_value.section(QLatin1Char(QLatin1Char('"')), 0, 0).remove(QLatin1Char('"')).trimmed();
|
||||
quotation_mark_end = true;
|
||||
}
|
||||
value.append(QLatin1Char(' ') + next_value);
|
||||
i = y;
|
||||
}
|
||||
if (!field.isEmpty() && !value.isEmpty()) {
|
||||
filters.insert(field, Filter(field, value));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (token.contains(operator_regex)) {
|
||||
QRegularExpressionMatch re_match = operator_regex.match(token);
|
||||
if (re_match.hasMatch()) {
|
||||
const QString foperator = re_match.captured(0);
|
||||
const QString field = token.section(foperator, 0, 0).remove(foperator).trimmed();
|
||||
const QString value = token.section(foperator, 1, -1).remove(foperator).trimmed();
|
||||
if (value.isEmpty()) continue;
|
||||
if (Song::kNumericalSearchColumns.contains(field, Qt::CaseInsensitive)) {
|
||||
if (Song::kIntSearchColumns.contains(field, Qt::CaseInsensitive)) {
|
||||
bool ok = false;
|
||||
const int value_int = value.toInt(&ok);
|
||||
if (ok) {
|
||||
filters.insert(field, Filter(field, value_int, foperator));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (Song::kUIntSearchColumns.contains(field, Qt::CaseInsensitive)) {
|
||||
bool ok = false;
|
||||
const uint value_uint = value.toUInt(&ok);
|
||||
if (ok) {
|
||||
filters.insert(field, Filter(field, value_uint, foperator));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (field.compare(QLatin1String("length"), Qt::CaseInsensitive) == 0) {
|
||||
filters.insert(field, Filter(field, static_cast<qint64>(Utilities::ParseSearchTime(value)) * kNsecPerSec, foperator));
|
||||
continue;
|
||||
}
|
||||
else if (field.compare(QLatin1String("rating"), Qt::CaseInsensitive) == 0) {
|
||||
filters.insert(field, Filter(field, Utilities::ParseSearchRating(value), foperator));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
filter_strings << token;
|
||||
}
|
||||
|
||||
if (filters.isEmpty() && filter_strings.isEmpty()) return true;
|
||||
|
||||
return item->metadata.is_valid() && ItemMetadataMatchesFilters(item->metadata, filters, filter_strings);
|
||||
|
||||
}
|
||||
|
||||
bool CollectionFilter::ItemMetadataMatchesFilters(const Song &metadata, const FilterList &filters, const QStringList &filter_strings) {
|
||||
|
||||
for (FilterList::const_iterator it = filters.begin() ; it != filters.end() ; ++it) {
|
||||
const QString &field = it.key();
|
||||
const Filter &filter = it.value();
|
||||
const QVariant &value = filter.value;
|
||||
const QString &foperator = filter.foperator;
|
||||
if (field.isEmpty() || !value.isValid()) {
|
||||
continue;
|
||||
}
|
||||
const QVariant data = DataFromField(field, metadata);
|
||||
if (
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
value.metaType() != data.metaType()
|
||||
const size_t hash = qHash(filter_string_);
|
||||
#else
|
||||
value.type() != data.type()
|
||||
const uint hash = qHash(filter_string_);
|
||||
#endif
|
||||
|| !FieldValueMatchesData(value, data, foperator)) {
|
||||
return false;
|
||||
}
|
||||
if (hash != query_hash_) {
|
||||
FilterParser p(filter_string_);
|
||||
filter_tree_.reset(p.parse());
|
||||
query_hash_ = hash;
|
||||
}
|
||||
|
||||
return filter_strings.isEmpty() || ItemMetadataMatchesFilterText(metadata, filter_strings);
|
||||
return item->metadata.is_valid() && filter_tree_->accept(item->metadata);
|
||||
|
||||
}
|
||||
|
||||
bool CollectionFilter::ItemMetadataMatchesFilterText(const Song &metadata, const QStringList &filter_strings) {
|
||||
void CollectionFilter::SetFilterString(const QString &filter_string) {
|
||||
|
||||
for (const QString &filter_string : filter_strings) {
|
||||
if (!metadata.effective_albumartist().contains(filter_string, Qt::CaseInsensitive) &&
|
||||
!metadata.artist().contains(filter_string, Qt::CaseInsensitive) &&
|
||||
!metadata.album().contains(filter_string, Qt::CaseInsensitive) &&
|
||||
!metadata.title().contains(filter_string, Qt::CaseInsensitive) &&
|
||||
!metadata.composer().contains(filter_string, Qt::CaseInsensitive) &&
|
||||
!metadata.performer().contains(filter_string, Qt::CaseInsensitive) &&
|
||||
!metadata.grouping().contains(filter_string, Qt::CaseInsensitive) &&
|
||||
!metadata.genre().contains(filter_string, Qt::CaseInsensitive) &&
|
||||
!metadata.comment().contains(filter_string, Qt::CaseInsensitive)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
QVariant CollectionFilter::DataFromField(const QString &field, const Song &metadata) {
|
||||
|
||||
if (field == QLatin1String("albumartist")) return metadata.effective_albumartist();
|
||||
if (field == QLatin1String("artist")) return metadata.artist();
|
||||
if (field == QLatin1String("album")) return metadata.album();
|
||||
if (field == QLatin1String("title")) return metadata.title();
|
||||
if (field == QLatin1String("composer")) return metadata.composer();
|
||||
if (field == QLatin1String("performer")) return metadata.performer();
|
||||
if (field == QLatin1String("grouping")) return metadata.grouping();
|
||||
if (field == QLatin1String("genre")) return metadata.genre();
|
||||
if (field == QLatin1String("comment")) return metadata.comment();
|
||||
if (field == QLatin1String("track")) return metadata.track();
|
||||
if (field == QLatin1String("year")) return metadata.year();
|
||||
if (field == QLatin1String("length")) return metadata.length_nanosec();
|
||||
if (field == QLatin1String("samplerate")) return metadata.samplerate();
|
||||
if (field == QLatin1String("bitdepth")) return metadata.bitdepth();
|
||||
if (field == QLatin1String("bitrate")) return metadata.bitrate();
|
||||
if (field == QLatin1String("rating")) return metadata.rating();
|
||||
if (field == QLatin1String("playcount")) return metadata.playcount();
|
||||
if (field == QLatin1String("skipcount")) return metadata.skipcount();
|
||||
|
||||
return QVariant();
|
||||
|
||||
}
|
||||
|
||||
bool CollectionFilter::FieldValueMatchesData(const QVariant &value, const QVariant &data, const QString &foperator) {
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
switch (value.metaType().id()) {
|
||||
#else
|
||||
switch (value.userType()) {
|
||||
#endif
|
||||
case QMetaType::QString:{
|
||||
const QString str_value = value.toString();
|
||||
const QString str_data = data.toString();
|
||||
return str_data.contains(str_value, Qt::CaseInsensitive);
|
||||
}
|
||||
case QMetaType::Int:{
|
||||
return FieldIntValueMatchesData(value.toInt(), foperator, data.toInt());
|
||||
}
|
||||
case QMetaType::UInt:{
|
||||
return FieldUIntValueMatchesData(value.toUInt(), foperator, data.toUInt());
|
||||
}
|
||||
case QMetaType::LongLong:{
|
||||
return FieldLongLongValueMatchesData(value.toLongLong(), foperator, data.toLongLong());
|
||||
}
|
||||
case QMetaType::Float:{
|
||||
return FieldFloatValueMatchesData(value.toFloat(), foperator, data.toFloat());
|
||||
}
|
||||
default:{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
bool CollectionFilter::FieldNumericalValueMatchesData(const T value, const QString &foperator, const T data) {
|
||||
|
||||
if (foperator == QLatin1Char('=') || foperator == QLatin1String("==")) {
|
||||
return data == value;
|
||||
}
|
||||
if (foperator == QLatin1String("!=") || foperator == QLatin1String("<>")) {
|
||||
return data != value;
|
||||
}
|
||||
if (foperator == QLatin1Char('<')) {
|
||||
return data < value;
|
||||
}
|
||||
if (foperator == QLatin1Char('>')) {
|
||||
return data > value;
|
||||
}
|
||||
if (foperator == QLatin1String(">=")) {
|
||||
return data >= value;
|
||||
}
|
||||
if (foperator == QLatin1String("<=")) {
|
||||
return data <= value;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
bool CollectionFilter::FieldIntValueMatchesData(const int value, const QString &foperator, const int data) {
|
||||
|
||||
return FieldNumericalValueMatchesData(value, foperator, data);
|
||||
|
||||
}
|
||||
|
||||
bool CollectionFilter::FieldUIntValueMatchesData(const uint value, const QString &foperator, const uint data) {
|
||||
|
||||
return FieldNumericalValueMatchesData(value, foperator, data);
|
||||
|
||||
}
|
||||
|
||||
bool CollectionFilter::FieldLongLongValueMatchesData(const qint64 value, const QString &foperator, const qint64 data) {
|
||||
|
||||
return FieldNumericalValueMatchesData(value, foperator, data);
|
||||
|
||||
}
|
||||
|
||||
bool CollectionFilter::FieldFloatValueMatchesData(const float value, const QString &foperator, const float data) {
|
||||
|
||||
return FieldNumericalValueMatchesData(value, foperator, data);
|
||||
|
||||
}
|
||||
|
||||
bool CollectionFilter::ContainsOperators(const QString &token) {
|
||||
|
||||
for (const QString &foperator : std::as_const(Operators)) {
|
||||
if (token.contains(foperator, Qt::CaseInsensitive)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
filter_string_ = filter_string;
|
||||
setFilterFixedString(filter_string);
|
||||
|
||||
}
|
||||
|
|
|
@ -22,16 +22,10 @@
|
|||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QScopedPointer>
|
||||
|
||||
#include "core/song.h"
|
||||
|
||||
class CollectionItem;
|
||||
#include "filterparser/filtertree.h"
|
||||
|
||||
class CollectionFilter : public QSortFilterProxyModel {
|
||||
Q_OBJECT
|
||||
|
@ -39,30 +33,20 @@ class CollectionFilter : public QSortFilterProxyModel {
|
|||
public:
|
||||
explicit CollectionFilter(QObject *parent = nullptr);
|
||||
|
||||
void SetFilterString(const QString &filter_string);
|
||||
QString filter_string() const { return filter_string_; }
|
||||
|
||||
protected:
|
||||
bool filterAcceptsRow(const int source_row, const QModelIndex &source_parent) const override;
|
||||
|
||||
private:
|
||||
static const QStringList Operators;
|
||||
struct Filter {
|
||||
public:
|
||||
Filter(const QString &_field = QString(), const QVariant &_value = QVariant(), const QString &_foperator = QString()) : field(_field), value(_value), foperator(_foperator) {}
|
||||
QString field;
|
||||
QVariant value;
|
||||
QString foperator;
|
||||
};
|
||||
using FilterList = QMap<QString, Filter>;
|
||||
static bool ItemMetadataMatchesFilters(const Song &metadata, const FilterList &filters, const QStringList &filter_strings);
|
||||
static bool ItemMetadataMatchesFilterText(const Song &metadata, const QStringList &filter_strings);
|
||||
static QVariant DataFromField(const QString &field, const Song &metadata);
|
||||
static bool FieldValueMatchesData(const QVariant &value, const QVariant &data, const QString &foperator);
|
||||
template<typename T>
|
||||
static bool FieldNumericalValueMatchesData(const T value, const QString &foperator, const T data);
|
||||
static bool FieldIntValueMatchesData(const int value, const QString &foperator, const int data);
|
||||
static bool FieldUIntValueMatchesData(const uint value, const QString &foperator, const uint data);
|
||||
static bool FieldLongLongValueMatchesData(const qint64 value, const QString &foperator, const qint64 data);
|
||||
static bool FieldFloatValueMatchesData(const float value, const QString &foperator, const float data);
|
||||
static bool ContainsOperators(const QString &token);
|
||||
mutable QScopedPointer<FilterTree> filter_tree_;
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
mutable size_t query_hash_;
|
||||
#else
|
||||
mutable uint query_hash_;
|
||||
#endif
|
||||
QString filter_string_;
|
||||
};
|
||||
|
||||
#endif // COLLECTIONFILTER_H
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
#include "collectionmodel.h"
|
||||
#include "collectionfilter.h"
|
||||
#include "collectionquery.h"
|
||||
#include "filterparser/filterparser.h"
|
||||
#include "savedgroupingmanager.h"
|
||||
#include "collectionfilterwidget.h"
|
||||
#include "groupbydialog.h"
|
||||
|
@ -71,47 +72,19 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
|
|||
group_by_menu_(nullptr),
|
||||
collection_menu_(nullptr),
|
||||
group_by_group_(nullptr),
|
||||
filter_delay_(new QTimer(this)),
|
||||
timer_filter_delay_(new QTimer(this)),
|
||||
filter_applies_to_model_(true),
|
||||
delay_behaviour_(DelayBehaviour::DelayedOnLargeLibraries) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
|
||||
QString available_fields = Song::kTextSearchColumns.join(QLatin1String(", "));
|
||||
available_fields += QLatin1String(", ") + Song::kNumericalSearchColumns.join(QLatin1String(", "));
|
||||
|
||||
ui_->search_field->setToolTip(
|
||||
QLatin1String("<html><head/><body><p>") +
|
||||
tr("Prefix a word with a field name to limit the search to that field, e.g.:") +
|
||||
QLatin1Char(' ') +
|
||||
QLatin1String("<span style=\"font-weight:600;\">") +
|
||||
tr("artist") +
|
||||
QLatin1String(":</span><span style=\"font-style:italic;\">Strawbs</span> ") +
|
||||
tr("searches the collection for all artists that contain the word %1. ").arg(QLatin1String("Strawbs")) +
|
||||
QLatin1String("</p><p>") +
|
||||
tr("Search terms for numerical fields can be prefixed with %1 or %2 to refine the search, e.g.: ")
|
||||
.arg(QLatin1String(" =, !=, <, >, <="), QLatin1String(">=")) +
|
||||
QLatin1String("<span style=\"font-weight:600;\">") +
|
||||
tr("rating") +
|
||||
QLatin1String("</span>") +
|
||||
QLatin1String(":>=") +
|
||||
QLatin1String("<span style=\"font-weight:italic;\">4</span>") +
|
||||
|
||||
QLatin1String("</p><p><span style=\"font-weight:600;\">") +
|
||||
tr("Available fields") +
|
||||
QLatin1String(": ") +
|
||||
QLatin1String("</span>") +
|
||||
QLatin1String("<span style=\"font-style:italic;\">") +
|
||||
available_fields +
|
||||
QLatin1String("</span>.") +
|
||||
QLatin1String("</p></body></html>")
|
||||
);
|
||||
ui_->search_field->setToolTip(FilterParser::ToolTip());
|
||||
|
||||
QObject::connect(ui_->search_field, &QSearchField::returnPressed, this, &CollectionFilterWidget::ReturnPressed);
|
||||
QObject::connect(filter_delay_, &QTimer::timeout, this, &CollectionFilterWidget::FilterDelayTimeout);
|
||||
QObject::connect(timer_filter_delay_, &QTimer::timeout, this, &CollectionFilterWidget::FilterDelayTimeout);
|
||||
|
||||
filter_delay_->setInterval(kFilterDelay);
|
||||
filter_delay_->setSingleShot(true);
|
||||
timer_filter_delay_->setInterval(kFilterDelay);
|
||||
timer_filter_delay_->setSingleShot(true);
|
||||
|
||||
// Icons
|
||||
ui_->options->setIcon(IconLoader::Load(QStringLiteral("configure")));
|
||||
|
@ -529,10 +502,10 @@ void CollectionFilterWidget::FilterTextChanged(const QString &text) {
|
|||
const bool delay = (delay_behaviour_ == DelayBehaviour::AlwaysDelayed) || (delay_behaviour_ == DelayBehaviour::DelayedOnLargeLibraries && !text.isEmpty() && text.length() < 3 && model_->total_song_count() >= 100000);
|
||||
|
||||
if (delay) {
|
||||
filter_delay_->start();
|
||||
timer_filter_delay_->start();
|
||||
}
|
||||
else {
|
||||
filter_delay_->stop();
|
||||
timer_filter_delay_->stop();
|
||||
FilterDelayTimeout();
|
||||
}
|
||||
|
||||
|
@ -541,7 +514,7 @@ void CollectionFilterWidget::FilterTextChanged(const QString &text) {
|
|||
void CollectionFilterWidget::FilterDelayTimeout() {
|
||||
|
||||
if (filter_applies_to_model_) {
|
||||
filter_->setFilterFixedString(ui_->search_field->text());
|
||||
filter_->SetFilterString(ui_->search_field->text());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -128,7 +128,7 @@ class CollectionFilterWidget : public QWidget {
|
|||
QActionGroup *group_by_group_;
|
||||
QHash<QAction*, int> filter_max_ages_;
|
||||
|
||||
QTimer *filter_delay_;
|
||||
QTimer *timer_filter_delay_;
|
||||
|
||||
bool filter_applies_to_model_;
|
||||
DelayBehaviour delay_behaviour_;
|
||||
|
|
|
@ -35,7 +35,6 @@
|
|||
|
||||
#include "collectionquery.h"
|
||||
#include "collectionfilteroptions.h"
|
||||
#include "utilities/searchparserutils.h"
|
||||
|
||||
CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const CollectionFilterOptions &filter_options)
|
||||
: SqlQuery(db),
|
||||
|
|
|
@ -0,0 +1,487 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2023, Daniel Ostertag <daniel.ostertag@dakes.de>
|
||||
*
|
||||
* Strawberry 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 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include "filterparser.h"
|
||||
#include "filtertree.h"
|
||||
#include "filterparsersearchcomparators.h"
|
||||
|
||||
FilterParser::FilterParser(const QString &filter_string) : filter_string_(filter_string), iter_{}, end_{} {}
|
||||
|
||||
FilterTree *FilterParser::parse() {
|
||||
|
||||
iter_ = filter_string_.constBegin();
|
||||
end_ = filter_string_.constEnd();
|
||||
|
||||
return parseOrGroup();
|
||||
|
||||
}
|
||||
|
||||
void FilterParser::advance() {
|
||||
|
||||
while (iter_ != end_ && iter_->isSpace()) {
|
||||
++iter_;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
FilterTree *FilterParser::parseOrGroup() {
|
||||
|
||||
advance();
|
||||
if (iter_ == end_) return new NopFilter;
|
||||
|
||||
OrFilter *group = new OrFilter;
|
||||
group->add(parseAndGroup());
|
||||
advance();
|
||||
while (checkOr()) {
|
||||
group->add(parseAndGroup());
|
||||
advance();
|
||||
}
|
||||
|
||||
return group;
|
||||
|
||||
}
|
||||
|
||||
FilterTree *FilterParser::parseAndGroup() {
|
||||
|
||||
advance();
|
||||
if (iter_ == end_) return new NopFilter;
|
||||
|
||||
AndFilter *group = new AndFilter();
|
||||
do {
|
||||
group->add(parseSearchExpression());
|
||||
advance();
|
||||
if (iter_ != end_ && *iter_ == QLatin1Char(')')) break;
|
||||
if (checkOr(false)) {
|
||||
break;
|
||||
}
|
||||
checkAnd(); // If there's no 'AND', we'll add the term anyway...
|
||||
} while (iter_ != end_);
|
||||
|
||||
return group;
|
||||
|
||||
}
|
||||
|
||||
bool FilterParser::checkAnd() {
|
||||
|
||||
if (iter_ != end_) {
|
||||
if (*iter_ == QLatin1Char('A')) {
|
||||
buf_ += *iter_;
|
||||
++iter_;
|
||||
if (iter_ != end_ && *iter_ == QLatin1Char('N')) {
|
||||
buf_ += *iter_;
|
||||
++iter_;
|
||||
if (iter_ != end_ && *iter_ == QLatin1Char('D')) {
|
||||
buf_ += *iter_;
|
||||
++iter_;
|
||||
if (iter_ != end_ && (iter_->isSpace() || *iter_ == QLatin1Char('-') || *iter_ == QLatin1Char('('))) {
|
||||
advance();
|
||||
buf_.clear();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
bool FilterParser::checkOr(const bool step_over) {
|
||||
|
||||
if (!buf_.isEmpty()) {
|
||||
if (buf_ == QLatin1String("OR")) {
|
||||
if (step_over) {
|
||||
buf_.clear();
|
||||
advance();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (iter_ != end_) {
|
||||
if (*iter_ == QLatin1Char('O')) {
|
||||
buf_ += *iter_;
|
||||
++iter_;
|
||||
if (iter_ != end_ && *iter_ == QLatin1Char('R')) {
|
||||
buf_ += *iter_;
|
||||
++iter_;
|
||||
if (iter_ != end_ && (iter_->isSpace() || *iter_ == QLatin1Char('-') || *iter_ == QLatin1Char('('))) {
|
||||
if (step_over) {
|
||||
buf_.clear();
|
||||
advance();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
FilterTree *FilterParser::parseSearchExpression() {
|
||||
|
||||
advance();
|
||||
if (iter_ == end_) return new NopFilter;
|
||||
if (*iter_ == QLatin1Char('(')) {
|
||||
++iter_;
|
||||
advance();
|
||||
FilterTree *tree = parseOrGroup();
|
||||
advance();
|
||||
if (iter_ != end_) {
|
||||
if (*iter_ == QLatin1Char(')')) {
|
||||
++iter_;
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
else if (*iter_ == QLatin1Char('-')) {
|
||||
++iter_;
|
||||
FilterTree *tree = parseSearchExpression();
|
||||
if (tree->type() != FilterTree::FilterType::Nop) return new NotFilter(tree);
|
||||
return tree;
|
||||
}
|
||||
else {
|
||||
return parseSearchTerm();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
FilterTree *FilterParser::parseSearchTerm() {
|
||||
|
||||
QString column;
|
||||
QString prefix;
|
||||
QString value;
|
||||
|
||||
bool in_quotes = false;
|
||||
|
||||
for (; iter_ != end_; ++iter_) {
|
||||
if (in_quotes) {
|
||||
if (*iter_ == QLatin1Char('"')) {
|
||||
in_quotes = false;
|
||||
}
|
||||
else {
|
||||
buf_ += *iter_;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (*iter_ == QLatin1Char('"')) {
|
||||
in_quotes = true;
|
||||
}
|
||||
else if (column.isEmpty() && *iter_ == QLatin1Char(':')) {
|
||||
column = buf_.toLower();
|
||||
buf_.clear();
|
||||
prefix.clear(); // Prefix isn't allowed here - let's ignore it
|
||||
}
|
||||
else if (iter_->isSpace() || *iter_ == QLatin1Char('(') || *iter_ == QLatin1Char(')') || *iter_ == QLatin1Char('-')) {
|
||||
break;
|
||||
}
|
||||
else if (buf_.isEmpty()) {
|
||||
// We don't know whether there is a column part in this search term thus we assume the latter and just try and read a prefix
|
||||
if (prefix.isEmpty() && (*iter_ == QLatin1Char('>') || *iter_ == QLatin1Char('<') || *iter_ == QLatin1Char('=') || *iter_ == QLatin1Char('!'))) {
|
||||
prefix += *iter_;
|
||||
}
|
||||
else if (prefix != QLatin1Char('=') && *iter_ == QLatin1Char('=')) {
|
||||
prefix += *iter_;
|
||||
}
|
||||
else {
|
||||
buf_ += *iter_;
|
||||
}
|
||||
}
|
||||
else {
|
||||
buf_ += *iter_;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
value = buf_.toLower();
|
||||
buf_.clear();
|
||||
|
||||
return createSearchTermTreeNode(column, prefix, value);
|
||||
|
||||
}
|
||||
|
||||
FilterTree *FilterParser::createSearchTermTreeNode(const QString &column, const QString &prefix, const QString &value) const {
|
||||
|
||||
if (value.isEmpty() && prefix != QLatin1Char('=')) {
|
||||
return new NopFilter;
|
||||
}
|
||||
|
||||
FilterParserSearchTermComparator *cmp = nullptr;
|
||||
|
||||
if (Song::kTextSearchColumns.contains(column, Qt::CaseInsensitive)) {
|
||||
if (prefix == QLatin1Char('=') || prefix == QLatin1String("==")) {
|
||||
cmp = new FilterParserTextEqComparator(value);
|
||||
}
|
||||
else if (prefix == QLatin1String("!=") || prefix == QLatin1String("<>")) {
|
||||
cmp = new FilterParserTextNeComparator(value);
|
||||
}
|
||||
else {
|
||||
cmp = new FilterParserDefaultComparator(value);
|
||||
}
|
||||
}
|
||||
else if (Song::kIntSearchColumns.contains(column, Qt::CaseInsensitive)) {
|
||||
bool ok = false;
|
||||
int number = value.toInt(&ok);
|
||||
if (ok) {
|
||||
if (prefix == QLatin1Char('=') || prefix == QLatin1String("==")) {
|
||||
cmp = new FilterParserIntEqComparator(number);
|
||||
}
|
||||
else if (prefix == QLatin1String("!=") || prefix == QLatin1String("<>")) {
|
||||
cmp = new FilterParserIntNeComparator(number);
|
||||
}
|
||||
else if (prefix == QLatin1Char('>')) {
|
||||
cmp = new FilterParserIntGtComparator(number);
|
||||
}
|
||||
else if (prefix == QLatin1String(">=")) {
|
||||
cmp = new FilterParserIntGeComparator(number);
|
||||
}
|
||||
else if (prefix == QLatin1Char('<')) {
|
||||
cmp = new FilterParserIntLtComparator(number);
|
||||
}
|
||||
else if (prefix == QLatin1String("<=")) {
|
||||
cmp = new FilterParserIntLeComparator(number);
|
||||
}
|
||||
else {
|
||||
cmp = new FilterParserIntEqComparator(number);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Song::kUIntSearchColumns.contains(column, Qt::CaseInsensitive)) {
|
||||
bool ok = false;
|
||||
uint number = value.toUInt(&ok);
|
||||
if (ok) {
|
||||
if (prefix == QLatin1Char('=') || prefix == QLatin1String("==")) {
|
||||
cmp = new FilterParserUIntEqComparator(number);
|
||||
}
|
||||
else if (prefix == QLatin1String("!=") || prefix == QLatin1String("<>")) {
|
||||
cmp = new FilterParserUIntNeComparator(number);
|
||||
}
|
||||
else if (prefix == QLatin1Char('>')) {
|
||||
cmp = new FilterParserUIntGtComparator(number);
|
||||
}
|
||||
else if (prefix == QLatin1String(">=")) {
|
||||
cmp = new FilterParserUIntGeComparator(number);
|
||||
}
|
||||
else if (prefix == QLatin1Char('<')) {
|
||||
cmp = new FilterParserUIntLtComparator(number);
|
||||
}
|
||||
else if (prefix == QLatin1String("<=")) {
|
||||
cmp = new FilterParserUIntLeComparator(number);
|
||||
}
|
||||
else {
|
||||
cmp = new FilterParserUIntEqComparator(number);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Song::kInt64SearchColumns.contains(column, Qt::CaseInsensitive)) {
|
||||
qint64 number = 0;
|
||||
if (column == QLatin1String("length")) {
|
||||
number = ParseTime(value);
|
||||
}
|
||||
else {
|
||||
number = value.toLongLong();
|
||||
}
|
||||
if (prefix == QLatin1Char('=') || prefix == QLatin1String("==")) {
|
||||
cmp = new FilterParserInt64EqComparator(number);
|
||||
}
|
||||
else if (prefix == QLatin1String("!=") || prefix == QLatin1String("<>")) {
|
||||
cmp = new FilterParserInt64NeComparator(number);
|
||||
}
|
||||
else if (prefix == QLatin1Char('>')) {
|
||||
cmp = new FilterParserInt64GtComparator(number);
|
||||
}
|
||||
else if (prefix == QLatin1String(">=")) {
|
||||
cmp = new FilterParserInt64GeComparator(number);
|
||||
}
|
||||
else if (prefix == QLatin1Char('<')) {
|
||||
cmp = new FilterParserInt64LtComparator(number);
|
||||
}
|
||||
else if (prefix == QLatin1String("<=")) {
|
||||
cmp = new FilterParserInt64LeComparator(number);
|
||||
}
|
||||
else {
|
||||
cmp = new FilterParserInt64EqComparator(number);
|
||||
}
|
||||
}
|
||||
else if (Song::kFloatSearchColumns.contains(column, Qt::CaseInsensitive)) {
|
||||
const float rating = ParseRating(value);
|
||||
if (prefix == QLatin1Char('=') || prefix == QLatin1String("==")) {
|
||||
cmp = new FilterParserFloatEqComparator(rating);
|
||||
}
|
||||
else if (prefix == QLatin1String("!=") || prefix == QLatin1String("<>")) {
|
||||
cmp = new FilterParserFloatNeComparator(rating);
|
||||
}
|
||||
else if (prefix == QLatin1Char('>')) {
|
||||
cmp = new FilterParserFloatGtComparator(rating);
|
||||
}
|
||||
else if (prefix == QLatin1String(">=")) {
|
||||
cmp = new FilterParserFloatGeComparator(rating);
|
||||
}
|
||||
else if (prefix == QLatin1Char('<')) {
|
||||
cmp = new FilterParserFloatLtComparator(rating);
|
||||
}
|
||||
else if (prefix == QLatin1String("<=")) {
|
||||
cmp = new FilterParserFloatLeComparator(rating);
|
||||
}
|
||||
else {
|
||||
cmp = new FilterParserFloatEqComparator(rating);
|
||||
}
|
||||
}
|
||||
|
||||
if (cmp) {
|
||||
return new FilterColumnTerm(column, cmp);
|
||||
}
|
||||
|
||||
return new FilterTerm(Song::kTextSearchColumns, new FilterParserDefaultComparator(value));
|
||||
|
||||
}
|
||||
|
||||
// Try and parse the string as '[[h:]m:]s' (ignoring all spaces),
|
||||
// and return the number of seconds if it parses correctly.
|
||||
// If not, the original string is returned.
|
||||
// The 'h', 'm' and 's' components can have any length (including 0).
|
||||
// A few examples:
|
||||
// "::" is parsed to "0"
|
||||
// "1::" is parsed to "3600"
|
||||
// "3:45" is parsed to "225"
|
||||
// "1:165" is parsed to "225"
|
||||
// "225" is parsed to "225" (srsly! ^.^)
|
||||
// "2:3:4:5" is parsed to "2:3:4:5"
|
||||
// "25m" is parsed to "25m"
|
||||
|
||||
qint64 FilterParser::ParseTime(const QString &time_str) {
|
||||
|
||||
qint64 seconds = 0;
|
||||
qint64 accum = 0;
|
||||
qint64 colon_count = 0;
|
||||
for (const QChar &c : time_str) {
|
||||
if (c.isDigit()) {
|
||||
accum = accum * 10LL + static_cast<qint64>(c.digitValue());
|
||||
}
|
||||
else if (c == QLatin1Char(':')) {
|
||||
seconds = seconds * 60LL + accum;
|
||||
accum = 0LL;
|
||||
++colon_count;
|
||||
if (colon_count > 2) {
|
||||
return 0LL;
|
||||
}
|
||||
}
|
||||
else if (!c.isSpace()) {
|
||||
return 0LL;
|
||||
}
|
||||
}
|
||||
seconds = seconds * 60LL + accum;
|
||||
|
||||
return seconds;
|
||||
|
||||
}
|
||||
|
||||
// Parses a rating search term to float.
|
||||
// If the rating is a number from 0-5, map it to 0-1
|
||||
// To use float values directly, the search term can be prefixed with "f" (rating:>f0.2)
|
||||
// If search string is 0, or by default, uses -1
|
||||
// @param rating_str: Rating search 0-5, or "f0.2"
|
||||
// @return float: rating from 0-1 or -1 if not rated.
|
||||
|
||||
float FilterParser::ParseRating(const QString &rating_str) {
|
||||
|
||||
if (rating_str.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
float rating = -1.0F;
|
||||
|
||||
// Check if the search is a float
|
||||
if (rating_str.contains(QLatin1Char('f'), Qt::CaseInsensitive)) {
|
||||
if (rating_str.count(QLatin1Char('f'), Qt::CaseInsensitive) > 1) {
|
||||
return rating;
|
||||
}
|
||||
QString rating_float_str = rating_str;
|
||||
if (rating_str.at(0) == QLatin1Char('f') || rating_str.at(0) == QLatin1Char('F')) {
|
||||
rating_float_str = rating_float_str.remove(0, 1);
|
||||
}
|
||||
if (rating_str.right(1) == QLatin1Char('f') || rating_str.right(1) == QLatin1Char('F')) {
|
||||
rating_float_str.chop(1);
|
||||
}
|
||||
bool ok = false;
|
||||
const float rating_input = rating_float_str.toFloat(&ok);
|
||||
if (ok) {
|
||||
rating = rating_input;
|
||||
}
|
||||
}
|
||||
else {
|
||||
bool ok = false;
|
||||
const int rating_input = rating_str.toInt(&ok);
|
||||
// Is valid int from 0-5: convert to float
|
||||
if (ok && rating_input >= 0 && rating_input <= 5) {
|
||||
rating = static_cast<float>(rating_input) / 5.0F;
|
||||
}
|
||||
}
|
||||
|
||||
// Songs with zero rating have -1 in the DB
|
||||
if (rating == 0) {
|
||||
rating = -1;
|
||||
}
|
||||
|
||||
return rating;
|
||||
|
||||
}
|
||||
|
||||
QString FilterParser::ToolTip() {
|
||||
|
||||
return QLatin1String("<html><head/><body><p>") +
|
||||
QObject::tr("Prefix a search term with a field name to limit the search to that field, e.g.:") +
|
||||
QLatin1Char(' ') +
|
||||
QLatin1String("<span style=\"font-weight:600;\">") +
|
||||
QObject::tr("artist") +
|
||||
QLatin1String(":</span><span style=\"font-style:italic;\">Strawbs</span> ") +
|
||||
QObject::tr("searches for all artists containing the word %1. ").arg(QLatin1String("Strawbs")) +
|
||||
QLatin1String("</p><p>") +
|
||||
|
||||
QObject::tr("Search terms for numerical fields can be prefixed with %1 or %2 to refine the search, e.g.: ")
|
||||
.arg(QLatin1String(" =, !=, <, >, <="), QLatin1String(">=")) +
|
||||
QLatin1String("<span style=\"font-weight:600;\">") +
|
||||
QObject::tr("rating") +
|
||||
QLatin1String("</span>") +
|
||||
QLatin1String(":>=") +
|
||||
QLatin1String("<span style=\"font-weight:italic;\">4</span>") +
|
||||
QLatin1String("</p><p>") +
|
||||
|
||||
QObject::tr("Multiple search terms can also be combined with \"%1\" (default) and \"%2\", as well as grouped with parentheses. ")
|
||||
.arg(QLatin1String("AND"), QLatin1String("OR")) +
|
||||
|
||||
QLatin1String("</p><p><span style=\"font-weight:600;\">") +
|
||||
QObject::tr("Available fields") +
|
||||
QLatin1String(": ") + QLatin1String("</span><span style=\"font-style:italic;\">") +
|
||||
Song::kSearchColumns.join(QLatin1String(", ")) +
|
||||
QLatin1String("</span>.") +
|
||||
QLatin1String("</p></body></html>");
|
||||
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2023, Daniel Ostertag <daniel.ostertag@dakes.de>
|
||||
*
|
||||
* Strawberry is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -19,47 +20,17 @@
|
|||
*
|
||||
*/
|
||||
|
||||
#ifndef PLAYLISTFILTERPARSER_H
|
||||
#define PLAYLISTFILTERPARSER_H
|
||||
#ifndef FILTERPARSER_H
|
||||
#define FILTERPARSER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QSet>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
|
||||
class QAbstractItemModel;
|
||||
class QModelIndex;
|
||||
|
||||
// Structure for filter parse tree
|
||||
class PlaylistFilterTree {
|
||||
public:
|
||||
PlaylistFilterTree() = default;
|
||||
virtual ~PlaylistFilterTree() {}
|
||||
virtual bool accept(const int row, const QModelIndex &parent, const QAbstractItemModel *const model) const = 0;
|
||||
enum class FilterType {
|
||||
Nop = 0,
|
||||
Or,
|
||||
And,
|
||||
Not,
|
||||
Column,
|
||||
Term
|
||||
};
|
||||
virtual FilterType type() = 0;
|
||||
private:
|
||||
Q_DISABLE_COPY(PlaylistFilterTree)
|
||||
};
|
||||
|
||||
// Trivial filter that accepts *anything*
|
||||
class PlaylistNopFilter : public PlaylistFilterTree {
|
||||
public:
|
||||
bool accept(const int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override { Q_UNUSED(row); Q_UNUSED(parent); Q_UNUSED(model); return true; }
|
||||
FilterType type() override { return FilterType::Nop; }
|
||||
};
|
||||
|
||||
class FilterTree;
|
||||
|
||||
// A utility class to parse search filter strings into a decision tree
|
||||
// that can decide whether a playlist entry matches the filter.
|
||||
// that can decide whether a song matches the filter.
|
||||
//
|
||||
// Here's a grammar describing the filters we expect:
|
||||
// expr ::= or-group
|
||||
|
@ -71,31 +42,35 @@ class PlaylistNopFilter : public PlaylistFilterTree {
|
|||
// string ::= [^:-()" ]+ | '"' [^"]+ '"'
|
||||
// prefix ::= '=' | '<' | '>' | '<=' | '>='
|
||||
// col ::= "title" | "artist" | ...
|
||||
class PlaylistFilterParser {
|
||||
class FilterParser {
|
||||
public:
|
||||
explicit PlaylistFilterParser(const QString &filter, const QMap<QString, int> &columns, const QSet<int> &numerical_cols);
|
||||
explicit FilterParser(const QString &filter_string);
|
||||
|
||||
PlaylistFilterTree *parse();
|
||||
FilterTree *parse();
|
||||
|
||||
private:
|
||||
static QString ToolTip();
|
||||
|
||||
protected:
|
||||
void advance();
|
||||
PlaylistFilterTree *parseOrGroup();
|
||||
PlaylistFilterTree *parseAndGroup();
|
||||
// Check if iter is at the start of 'AND' if so, step over it and return true if not, return false and leave iter where it was
|
||||
bool checkAnd();
|
||||
// Check if iter is at the start of 'OR'
|
||||
bool checkOr(const bool step_over = true);
|
||||
PlaylistFilterTree *parseSearchExpression();
|
||||
PlaylistFilterTree *parseSearchTerm();
|
||||
|
||||
PlaylistFilterTree *createSearchTermTreeNode(const QString &col, const QString &prefix, const QString &search) const;
|
||||
FilterTree *parseOrGroup();
|
||||
FilterTree *parseAndGroup();
|
||||
FilterTree *parseSearchExpression();
|
||||
FilterTree *parseSearchTerm();
|
||||
|
||||
FilterTree *createSearchTermTreeNode(const QString &column, const QString &prefix, const QString &value) const;
|
||||
|
||||
static qint64 ParseTime(const QString &time_str);
|
||||
static float ParseRating(const QString &rating_str);
|
||||
|
||||
const QString filter_string_;
|
||||
QString::const_iterator iter_;
|
||||
QString::const_iterator end_;
|
||||
QString buf_;
|
||||
const QString filterstring_;
|
||||
const QMap<QString, int> columns_;
|
||||
const QSet<int> numerical_columns_;
|
||||
};
|
||||
|
||||
#endif // PLAYLISTFILTERPARSER_H
|
||||
#endif // FILTERPARSER_H
|
|
@ -0,0 +1,324 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry 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 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef FILTERPARSERSEARCHCOMPARATORS_H
|
||||
#define FILTERPARSERSEARCHCOMPARATORS_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QScopedPointer>
|
||||
|
||||
class FilterParserSearchTermComparator {
|
||||
public:
|
||||
FilterParserSearchTermComparator() = default;
|
||||
virtual ~FilterParserSearchTermComparator() = default;
|
||||
virtual bool Matches(const QVariant &value) const = 0;
|
||||
private:
|
||||
Q_DISABLE_COPY(FilterParserSearchTermComparator)
|
||||
};
|
||||
|
||||
// "compares" by checking if the field contains the search term
|
||||
class FilterParserDefaultComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserDefaultComparator(const QString &search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toString().contains(search_term_, Qt::CaseInsensitive);
|
||||
}
|
||||
private:
|
||||
QString search_term_;
|
||||
|
||||
Q_DISABLE_COPY(FilterParserDefaultComparator)
|
||||
};
|
||||
|
||||
class FilterParserTextEqComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserTextEqComparator(const QString &search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return search_term_.compare(value.toString(), Qt::CaseInsensitive) == 0;
|
||||
}
|
||||
private:
|
||||
QString search_term_;
|
||||
};
|
||||
|
||||
class FilterParserTextNeComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserTextNeComparator(const QString &search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return search_term_.compare(value.toString(), Qt::CaseInsensitive) != 0;
|
||||
}
|
||||
private:
|
||||
QString search_term_;
|
||||
};
|
||||
|
||||
class FilterParserIntEqComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserIntEqComparator(const int search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toInt() == search_term_;
|
||||
}
|
||||
private:
|
||||
int search_term_;
|
||||
};
|
||||
|
||||
class FilterParserIntNeComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserIntNeComparator(const int search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toInt() != search_term_;
|
||||
}
|
||||
private:
|
||||
int search_term_;
|
||||
};
|
||||
|
||||
class FilterParserIntGtComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserIntGtComparator(const int search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toInt() > search_term_;
|
||||
}
|
||||
private:
|
||||
int search_term_;
|
||||
};
|
||||
|
||||
class FilterParserIntGeComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserIntGeComparator(const int search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toInt() >= search_term_;
|
||||
}
|
||||
private:
|
||||
int search_term_;
|
||||
};
|
||||
|
||||
class FilterParserIntLtComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserIntLtComparator(const int search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toInt() < search_term_;
|
||||
}
|
||||
private:
|
||||
int search_term_;
|
||||
};
|
||||
|
||||
class FilterParserIntLeComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserIntLeComparator(const int search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toInt() <= search_term_;
|
||||
}
|
||||
private:
|
||||
int search_term_;
|
||||
};
|
||||
|
||||
class FilterParserUIntEqComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserUIntEqComparator(const uint search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toUInt() == search_term_;
|
||||
}
|
||||
private:
|
||||
uint search_term_;
|
||||
};
|
||||
|
||||
class FilterParserUIntNeComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserUIntNeComparator(const uint search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toUInt() != search_term_;
|
||||
}
|
||||
private:
|
||||
uint search_term_;
|
||||
};
|
||||
|
||||
class FilterParserUIntGtComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserUIntGtComparator(const uint search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toUInt() > search_term_;
|
||||
}
|
||||
private:
|
||||
uint search_term_;
|
||||
};
|
||||
|
||||
class FilterParserUIntGeComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserUIntGeComparator(const uint search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toUInt() >= search_term_;
|
||||
}
|
||||
private:
|
||||
uint search_term_;
|
||||
};
|
||||
|
||||
class FilterParserUIntLtComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserUIntLtComparator(const uint search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toUInt() < search_term_;
|
||||
}
|
||||
private:
|
||||
uint search_term_;
|
||||
};
|
||||
|
||||
class FilterParserUIntLeComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserUIntLeComparator(const uint search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toUInt() <= search_term_;
|
||||
}
|
||||
private:
|
||||
uint search_term_;
|
||||
};
|
||||
|
||||
class FilterParserInt64EqComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserInt64EqComparator(const qint64 search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toLongLong() == search_term_;
|
||||
}
|
||||
private:
|
||||
qint64 search_term_;
|
||||
};
|
||||
|
||||
class FilterParserInt64NeComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserInt64NeComparator(const qint64 search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toLongLong() != search_term_;
|
||||
}
|
||||
private:
|
||||
qint64 search_term_;
|
||||
};
|
||||
|
||||
class FilterParserInt64GtComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserInt64GtComparator(const qint64 search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toLongLong() > search_term_;
|
||||
}
|
||||
private:
|
||||
qint64 search_term_;
|
||||
};
|
||||
|
||||
class FilterParserInt64GeComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserInt64GeComparator(const qint64 search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toLongLong() >= search_term_;
|
||||
}
|
||||
private:
|
||||
qint64 search_term_;
|
||||
};
|
||||
|
||||
class FilterParserInt64LtComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserInt64LtComparator(const qint64 search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toLongLong() < search_term_;
|
||||
}
|
||||
private:
|
||||
qint64 search_term_;
|
||||
};
|
||||
|
||||
class FilterParserInt64LeComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserInt64LeComparator(const qint64 search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toLongLong() <= search_term_;
|
||||
}
|
||||
private:
|
||||
qint64 search_term_;
|
||||
};
|
||||
|
||||
// Float Comparators are for the rating
|
||||
class FilterParserFloatEqComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserFloatEqComparator(const float search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toFloat() == search_term_;
|
||||
}
|
||||
private:
|
||||
float search_term_;
|
||||
};
|
||||
|
||||
class FilterParserFloatNeComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserFloatNeComparator(const float value) : search_term_(value) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toFloat() != search_term_;
|
||||
}
|
||||
private:
|
||||
float search_term_;
|
||||
};
|
||||
|
||||
class FilterParserFloatGtComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserFloatGtComparator(const float search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toFloat() > search_term_;
|
||||
}
|
||||
private:
|
||||
float search_term_;
|
||||
};
|
||||
|
||||
class FilterParserFloatGeComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserFloatGeComparator(const float search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toFloat() >= search_term_;
|
||||
}
|
||||
private:
|
||||
float search_term_;
|
||||
};
|
||||
|
||||
class FilterParserFloatLtComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserFloatLtComparator(const float search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toFloat() < search_term_;
|
||||
}
|
||||
private:
|
||||
float search_term_;
|
||||
};
|
||||
|
||||
class FilterParserFloatLeComparator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserFloatLeComparator(const float search_term) : search_term_(search_term) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return value.toFloat() <= search_term_;
|
||||
}
|
||||
private:
|
||||
float search_term_;
|
||||
};
|
||||
|
||||
class FilterParserRatingComparatorDecorator : public FilterParserSearchTermComparator {
|
||||
public:
|
||||
explicit FilterParserRatingComparatorDecorator(FilterParserSearchTermComparator *cmp) : cmp_(cmp) {}
|
||||
bool Matches(const QVariant &value) const override {
|
||||
return cmp_->Matches(QString::number(lround(value.toDouble() * 10.0)));
|
||||
}
|
||||
private:
|
||||
QScopedPointer<FilterParserSearchTermComparator> cmp_;
|
||||
};
|
||||
|
||||
#endif // FILTERPARSERSEARCHCOMPARATORS_H
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry 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 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include "filtertree.h"
|
||||
|
||||
FilterTree::FilterTree() = default;
|
||||
FilterTree::~FilterTree() = default;
|
||||
|
||||
QVariant FilterTree::DataFromColumn(const QString &column, const Song &metadata) {
|
||||
|
||||
if (column == QLatin1String("albumartist")) return metadata.effective_albumartist();
|
||||
if (column == QLatin1String("artist")) return metadata.artist();
|
||||
if (column == QLatin1String("album")) return metadata.album();
|
||||
if (column == QLatin1String("title")) return metadata.title();
|
||||
if (column == QLatin1String("composer")) return metadata.composer();
|
||||
if (column == QLatin1String("performer")) return metadata.performer();
|
||||
if (column == QLatin1String("grouping")) return metadata.grouping();
|
||||
if (column == QLatin1String("genre")) return metadata.genre();
|
||||
if (column == QLatin1String("comment")) return metadata.comment();
|
||||
if (column == QLatin1String("track")) return metadata.track();
|
||||
if (column == QLatin1String("year")) return metadata.year();
|
||||
if (column == QLatin1String("length")) return metadata.length_nanosec();
|
||||
if (column == QLatin1String("samplerate")) return metadata.samplerate();
|
||||
if (column == QLatin1String("bitdepth")) return metadata.bitdepth();
|
||||
if (column == QLatin1String("bitrate")) return metadata.bitrate();
|
||||
if (column == QLatin1String("rating")) return metadata.rating();
|
||||
if (column == QLatin1String("playcount")) return metadata.playcount();
|
||||
if (column == QLatin1String("skipcount")) return metadata.skipcount();
|
||||
|
||||
return QVariant();
|
||||
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry 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 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef FILTERTREE_H
|
||||
#define FILTERTREE_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QScopedPointer>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "filterparsersearchcomparators.h"
|
||||
|
||||
class FilterTree {
|
||||
public:
|
||||
explicit FilterTree();
|
||||
virtual ~FilterTree();
|
||||
|
||||
enum class FilterType {
|
||||
Nop = 0,
|
||||
Or,
|
||||
And,
|
||||
Not,
|
||||
Column,
|
||||
Term
|
||||
};
|
||||
|
||||
virtual FilterType type() const = 0;
|
||||
|
||||
virtual bool accept(const Song &song) const = 0;
|
||||
|
||||
protected:
|
||||
static QVariant DataFromColumn(const QString &column, const Song &metadata);
|
||||
|
||||
private:
|
||||
Q_DISABLE_COPY(FilterTree)
|
||||
};
|
||||
|
||||
// Trivial filter that accepts *anything*
|
||||
class NopFilter : public FilterTree {
|
||||
public:
|
||||
FilterType type() const override { return FilterType::Nop; }
|
||||
bool accept(const Song &song) const override { Q_UNUSED(song); return true; }
|
||||
};
|
||||
|
||||
// Filter that applies a SearchTermComparator to all fields
|
||||
class FilterTerm : public FilterTree {
|
||||
public:
|
||||
explicit FilterTerm(const QStringList &columns, FilterParserSearchTermComparator *comparator) : columns_(columns), cmp_(comparator) {}
|
||||
|
||||
FilterType type() const override { return FilterType::Term; }
|
||||
|
||||
bool accept(const Song &song) const override {
|
||||
for (const QString &column : columns_) {
|
||||
if (cmp_->Matches(DataFromColumn(column, song))) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
const QStringList columns_;
|
||||
QScopedPointer<FilterParserSearchTermComparator> cmp_;
|
||||
};
|
||||
|
||||
class FilterColumnTerm : public FilterTree {
|
||||
public:
|
||||
explicit FilterColumnTerm(const QString &column, FilterParserSearchTermComparator *comparator) : column_(column), cmp_(comparator) {}
|
||||
|
||||
FilterType type() const override { return FilterType::Column; }
|
||||
|
||||
bool accept(const Song &song) const override {
|
||||
return cmp_->Matches(DataFromColumn(column_, song));
|
||||
}
|
||||
|
||||
private:
|
||||
const QString column_;
|
||||
QScopedPointer<FilterParserSearchTermComparator> cmp_;
|
||||
};
|
||||
|
||||
class NotFilter : public FilterTree {
|
||||
public:
|
||||
explicit NotFilter(const FilterTree *inv) : child_(inv) {}
|
||||
|
||||
FilterType type() const override { return FilterType::Not; }
|
||||
|
||||
bool accept(const Song &song) const override {
|
||||
return !child_->accept(song);
|
||||
}
|
||||
|
||||
private:
|
||||
QScopedPointer<const FilterTree> child_;
|
||||
};
|
||||
|
||||
class OrFilter : public FilterTree {
|
||||
public:
|
||||
~OrFilter() override { qDeleteAll(children_); }
|
||||
|
||||
FilterType type() const override { return FilterType::Or; }
|
||||
|
||||
virtual void add(FilterTree *child) { children_.append(child); }
|
||||
|
||||
bool accept(const Song &song) const override {
|
||||
return std::any_of(children_.begin(), children_.end(), [song](FilterTree *child) { return child->accept(song); });
|
||||
}
|
||||
|
||||
private:
|
||||
QList<FilterTree*> children_;
|
||||
};
|
||||
|
||||
class AndFilter : public FilterTree {
|
||||
public:
|
||||
~AndFilter() override { qDeleteAll(children_); }
|
||||
|
||||
FilterType type() const override { return FilterType::And; }
|
||||
|
||||
virtual void add(FilterTree *child) { children_.append(child); }
|
||||
|
||||
bool accept(const Song &song) const override {
|
||||
return !std::any_of(children_.begin(), children_.end(), [song](FilterTree *child) { return !child->accept(song); });
|
||||
}
|
||||
|
||||
private:
|
||||
QList<FilterTree*> children_;
|
||||
};
|
||||
|
||||
#endif // FILTERTREE_H
|
|
@ -49,6 +49,7 @@
|
|||
#include "core/shared_ptr.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "core/settings.h"
|
||||
#include "filterparser/filterparser.h"
|
||||
#include "playlist.h"
|
||||
#include "playlisttabbar.h"
|
||||
#include "playlistview.h"
|
||||
|
@ -124,37 +125,7 @@ PlaylistContainer::PlaylistContainer(QWidget *parent)
|
|||
QObject::connect(ui_->playlist, &PlaylistView::FocusOnFilterSignal, this, &PlaylistContainer::FocusOnFilter);
|
||||
ui_->search_field->installEventFilter(this);
|
||||
|
||||
QString available_fields = PlaylistFilter().column_names().keys().join(QLatin1String(", "));
|
||||
ui_->search_field->setToolTip(
|
||||
QLatin1String("<html><head/><body><p>") +
|
||||
tr("Prefix a search term with a field name to limit the search to that field, e.g.:") +
|
||||
QLatin1Char(' ') +
|
||||
QLatin1String("<span style=\"font-weight:600;\">") +
|
||||
tr("artist") +
|
||||
QLatin1String(":</span><span style=\"font-style:italic;\">Strawbs</span> ") +
|
||||
tr("searches the playlist for all artists that contain the word %1. ").arg(QLatin1String("Strawbs")) +
|
||||
QLatin1String("</p><p>") +
|
||||
|
||||
tr("Search terms for numerical fields can be prefixed with %1 or %2 to refine the search, e.g.: ")
|
||||
.arg(QLatin1String(" =, !=, <, >, <="), QLatin1String(">=")) +
|
||||
QLatin1String("<span style=\"font-weight:600;\">") +
|
||||
tr("rating") +
|
||||
QLatin1String("</span>") +
|
||||
QLatin1String(":>=") +
|
||||
QLatin1String("<span style=\"font-weight:italic;\">4</span>") +
|
||||
QLatin1String("</p><p>") +
|
||||
|
||||
tr("Multiple search terms can also be combined with \"%1\" (default) and \"%2\", as well as grouped with parentheses. ")
|
||||
.arg(QLatin1String("AND"), QLatin1String("OR")) +
|
||||
|
||||
QLatin1String("</p><p><span style=\"font-weight:600;\">") +
|
||||
tr("Available fields") +
|
||||
QLatin1String(": ") + QLatin1String("</span><span style=\"font-style:italic;\">") +
|
||||
available_fields +
|
||||
QLatin1String("</span>.") +
|
||||
QLatin1String("</p></body></html>")
|
||||
);
|
||||
|
||||
ui_->search_field->setToolTip(FilterParser::ToolTip());
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
|
@ -234,7 +205,7 @@ void PlaylistContainer::SetViewModel(Playlist *playlist, const int scroll_positi
|
|||
emit ViewSelectionModelChanged();
|
||||
|
||||
// Update filter
|
||||
ui_->search_field->setText(playlist->filter()->filter_text());
|
||||
ui_->search_field->setText(playlist->filter()->filter_string());
|
||||
|
||||
// Update the no matches label
|
||||
QObject::connect(playlist_->filter(), &QSortFilterProxyModel::modelReset, this, &PlaylistContainer::UpdateNoMatchesLabel);
|
||||
|
@ -452,7 +423,7 @@ void PlaylistContainer::UpdateFilter() {
|
|||
|
||||
if (!ui_->toolbar->isVisible()) return;
|
||||
|
||||
manager_->current()->filter()->SetFilterText(ui_->search_field->text());
|
||||
manager_->current()->filter()->SetFilterString(ui_->search_field->text());
|
||||
ui_->playlist->JumpToCurrentlyPlayingTrack();
|
||||
|
||||
UpdateNoMatchesLabel();
|
||||
|
|
|
@ -23,54 +23,20 @@
|
|||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QAbstractItemModel>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
#include "playlist/playlist.h"
|
||||
#include "playlist/playlistitem.h"
|
||||
#include "filterparser/filterparser.h"
|
||||
#include "filterparser/filtertree.h"
|
||||
#include "playlistfilter.h"
|
||||
#include "playlistfilterparser.h"
|
||||
|
||||
PlaylistFilter::PlaylistFilter(QObject *parent)
|
||||
: QSortFilterProxyModel(parent),
|
||||
filter_tree_(new PlaylistNopFilter),
|
||||
filter_tree_(new NopFilter),
|
||||
query_hash_(0) {
|
||||
|
||||
setDynamicSortFilter(true);
|
||||
|
||||
column_names_[QStringLiteral("title")] = static_cast<int>(Playlist::Column::Title);
|
||||
column_names_[QStringLiteral("name")] = static_cast<int>(Playlist::Column::Title);
|
||||
column_names_[QStringLiteral("artist")] = static_cast<int>(Playlist::Column::Artist);
|
||||
column_names_[QStringLiteral("album")] = static_cast<int>(Playlist::Column::Album);
|
||||
column_names_[QStringLiteral("albumartist")] = static_cast<int>(Playlist::Column::AlbumArtist);
|
||||
column_names_[QStringLiteral("performer")] = static_cast<int>(Playlist::Column::Performer);
|
||||
column_names_[QStringLiteral("composer")] = static_cast<int>(Playlist::Column::Composer);
|
||||
column_names_[QStringLiteral("year")] = static_cast<int>(Playlist::Column::Year);
|
||||
column_names_[QStringLiteral("originalyear")] = static_cast<int>(Playlist::Column::OriginalYear);
|
||||
column_names_[QStringLiteral("track")] = static_cast<int>(Playlist::Column::Track);
|
||||
column_names_[QStringLiteral("disc")] = static_cast<int>(Playlist::Column::Disc);
|
||||
column_names_[QStringLiteral("length")] = static_cast<int>(Playlist::Column::Length);
|
||||
column_names_[QStringLiteral("genre")] = static_cast<int>(Playlist::Column::Genre);
|
||||
column_names_[QStringLiteral("samplerate")] = static_cast<int>(Playlist::Column::Samplerate);
|
||||
column_names_[QStringLiteral("bitdepth")] = static_cast<int>(Playlist::Column::Bitdepth);
|
||||
column_names_[QStringLiteral("bitrate")] = static_cast<int>(Playlist::Column::Bitrate);
|
||||
column_names_[QStringLiteral("filename")] = static_cast<int>(Playlist::Column::Filename);
|
||||
column_names_[QStringLiteral("grouping")] = static_cast<int>(Playlist::Column::Grouping);
|
||||
column_names_[QStringLiteral("comment")] = static_cast<int>(Playlist::Column::Comment);
|
||||
column_names_[QStringLiteral("rating")] = static_cast<int>(Playlist::Column::Rating);
|
||||
column_names_[QStringLiteral("playcount")] = static_cast<int>(Playlist::Column::PlayCount);
|
||||
column_names_[QStringLiteral("skipcount")] = static_cast<int>(Playlist::Column::SkipCount);
|
||||
|
||||
numerical_columns_ << static_cast<int>(Playlist::Column::Year)
|
||||
<< static_cast<int>(Playlist::Column::OriginalYear)
|
||||
<< static_cast<int>(Playlist::Column::Track)
|
||||
<< static_cast<int>(Playlist::Column::Disc)
|
||||
<< static_cast<int>(Playlist::Column::Length)
|
||||
<< static_cast<int>(Playlist::Column::Samplerate)
|
||||
<< static_cast<int>(Playlist::Column::Bitdepth)
|
||||
<< static_cast<int>(Playlist::Column::Bitrate)
|
||||
<< static_cast<int>(Playlist::Column::PlayCount)
|
||||
<< static_cast<int>(Playlist::Column::SkipCount);
|
||||
|
||||
}
|
||||
|
||||
PlaylistFilter::~PlaylistFilter() = default;
|
||||
|
@ -80,29 +46,35 @@ void PlaylistFilter::sort(int column, Qt::SortOrder order) {
|
|||
sourceModel()->sort(column, order);
|
||||
}
|
||||
|
||||
bool PlaylistFilter::filterAcceptsRow(const int row, const QModelIndex &parent) const {
|
||||
bool PlaylistFilter::filterAcceptsRow(const int source_row, const QModelIndex &source_parent) const {
|
||||
|
||||
Playlist *playlist = qobject_cast<Playlist*>(sourceModel());
|
||||
if (!playlist) return false;
|
||||
const QModelIndex idx = sourceModel()->index(source_row, 0, source_parent);
|
||||
if (!idx.isValid()) return false;
|
||||
PlaylistItemPtr item = playlist->item_at(idx.row());
|
||||
if (!item) return false;
|
||||
|
||||
if (filter_string_.isEmpty()) return true;
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
size_t hash = qHash(filter_text_);
|
||||
const size_t hash = qHash(filter_string_);
|
||||
#else
|
||||
uint hash = qHash(filter_text_);
|
||||
const uint hash = qHash(filter_string_);
|
||||
#endif
|
||||
if (hash != query_hash_) {
|
||||
// Parse the query
|
||||
PlaylistFilterParser p(filter_text_, column_names_, numerical_columns_);
|
||||
FilterParser p(filter_string_);
|
||||
filter_tree_.reset(p.parse());
|
||||
|
||||
query_hash_ = hash;
|
||||
}
|
||||
|
||||
// Test the row
|
||||
return filter_tree_->accept(row, parent, sourceModel());
|
||||
return filter_tree_->accept(item->Metadata());
|
||||
|
||||
}
|
||||
|
||||
void PlaylistFilter::SetFilterText(const QString &filter_text) {
|
||||
void PlaylistFilter::SetFilterString(const QString &filter_string) {
|
||||
|
||||
filter_text_ = filter_text;
|
||||
setFilterFixedString(filter_text);
|
||||
filter_string_ = filter_string;
|
||||
setFilterFixedString(filter_string);
|
||||
|
||||
}
|
||||
|
|
|
@ -24,15 +24,11 @@
|
|||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QMap>
|
||||
#include <QSet>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QScopedPointer>
|
||||
#include <QString>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
class PlaylistFilterTree;
|
||||
#include "filterparser/filtertree.h"
|
||||
|
||||
class PlaylistFilter : public QSortFilterProxyModel {
|
||||
Q_OBJECT
|
||||
|
@ -48,23 +44,18 @@ class PlaylistFilter : public QSortFilterProxyModel {
|
|||
// public so Playlist::NextVirtualIndex and friends can get at it
|
||||
bool filterAcceptsRow(const int source_row, const QModelIndex &source_parent) const override;
|
||||
|
||||
void SetFilterText(const QString &filter_text);
|
||||
|
||||
QString filter_text() const { return filter_text_; }
|
||||
QMap<QString, int> column_names() const { return column_names_; }
|
||||
void SetFilterString(const QString &filter_string);
|
||||
QString filter_string() const { return filter_string_; }
|
||||
|
||||
private:
|
||||
// Mutable because they're modified from filterAcceptsRow() const
|
||||
mutable QScopedPointer<PlaylistFilterTree> filter_tree_;
|
||||
mutable QScopedPointer<FilterTree> filter_tree_;
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
mutable size_t query_hash_;
|
||||
#else
|
||||
mutable uint query_hash_;
|
||||
#endif
|
||||
|
||||
QMap<QString, int> column_names_;
|
||||
QSet<int> numerical_columns_;
|
||||
QString filter_text_;
|
||||
QString filter_string_;
|
||||
};
|
||||
|
||||
#endif // PLAYLISTFILTER_H
|
||||
|
|
|
@ -1,610 +0,0 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
*
|
||||
* Strawberry 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 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QSet>
|
||||
#include <QChar>
|
||||
#include <QScopedPointer>
|
||||
#include <QString>
|
||||
#include <QtAlgorithms>
|
||||
#include <QAbstractItemModel>
|
||||
|
||||
#include "playlist.h"
|
||||
#include "playlistfilterparser.h"
|
||||
#include "utilities/searchparserutils.h"
|
||||
|
||||
class PlaylistSearchTermComparator {
|
||||
public:
|
||||
PlaylistSearchTermComparator() = default;
|
||||
virtual ~PlaylistSearchTermComparator() = default;
|
||||
virtual bool Matches(const QString &element) const = 0;
|
||||
private:
|
||||
Q_DISABLE_COPY(PlaylistSearchTermComparator)
|
||||
};
|
||||
|
||||
// "compares" by checking if the field contains the search term
|
||||
class PlaylistDefaultComparator : public PlaylistSearchTermComparator {
|
||||
public:
|
||||
explicit PlaylistDefaultComparator(const QString &value) : search_term_(value) {}
|
||||
bool Matches(const QString &element) const override {
|
||||
return element.contains(search_term_);
|
||||
}
|
||||
private:
|
||||
QString search_term_;
|
||||
|
||||
Q_DISABLE_COPY(PlaylistDefaultComparator)
|
||||
};
|
||||
|
||||
class PlaylistEqComparator : public PlaylistSearchTermComparator {
|
||||
public:
|
||||
explicit PlaylistEqComparator(const QString &value) : search_term_(value) {}
|
||||
bool Matches(const QString &element) const override {
|
||||
return search_term_ == element;
|
||||
}
|
||||
private:
|
||||
QString search_term_;
|
||||
};
|
||||
|
||||
class PlaylistNeComparator : public PlaylistSearchTermComparator {
|
||||
public:
|
||||
explicit PlaylistNeComparator(const QString &value) : search_term_(value) {}
|
||||
bool Matches(const QString &element) const override {
|
||||
return search_term_ != element;
|
||||
}
|
||||
private:
|
||||
QString search_term_;
|
||||
};
|
||||
|
||||
class PlaylistLexicalGtComparator : public PlaylistSearchTermComparator {
|
||||
public:
|
||||
explicit PlaylistLexicalGtComparator(const QString &value) : search_term_(value) {}
|
||||
bool Matches(const QString &element) const override {
|
||||
return element > search_term_;
|
||||
}
|
||||
private:
|
||||
QString search_term_;
|
||||
};
|
||||
|
||||
class PlaylistLexicalGeComparator : public PlaylistSearchTermComparator {
|
||||
public:
|
||||
explicit PlaylistLexicalGeComparator(const QString &value) : search_term_(value) {}
|
||||
bool Matches(const QString &element) const override {
|
||||
return element >= search_term_;
|
||||
}
|
||||
private:
|
||||
QString search_term_;
|
||||
};
|
||||
|
||||
class PlaylistLexicalLtComparator : public PlaylistSearchTermComparator {
|
||||
public:
|
||||
explicit PlaylistLexicalLtComparator(const QString &value) : search_term_(value) {}
|
||||
bool Matches(const QString &element) const override {
|
||||
return element < search_term_;
|
||||
}
|
||||
private:
|
||||
QString search_term_;
|
||||
};
|
||||
|
||||
class PlaylistLexicalLeComparator : public PlaylistSearchTermComparator {
|
||||
public:
|
||||
explicit PlaylistLexicalLeComparator(const QString &value) : search_term_(value) {}
|
||||
bool Matches(const QString &element) const override {
|
||||
return element <= search_term_;
|
||||
}
|
||||
private:
|
||||
QString search_term_;
|
||||
};
|
||||
|
||||
// Float Comparators are for the rating
|
||||
class PlaylistFloatEqComparator : public PlaylistSearchTermComparator {
|
||||
public:
|
||||
explicit PlaylistFloatEqComparator(const float value) : search_term_(value) {}
|
||||
bool Matches(const QString &element) const override {
|
||||
return search_term_ == element.toFloat();
|
||||
}
|
||||
private:
|
||||
float search_term_;
|
||||
};
|
||||
|
||||
class PlaylistFloatNeComparator : public PlaylistSearchTermComparator {
|
||||
public:
|
||||
explicit PlaylistFloatNeComparator(const float value) : search_term_(value) {}
|
||||
bool Matches(const QString &element) const override {
|
||||
return search_term_ != element.toFloat();
|
||||
}
|
||||
private:
|
||||
float search_term_;
|
||||
};
|
||||
|
||||
class PlaylistFloatGtComparator : public PlaylistSearchTermComparator {
|
||||
public:
|
||||
explicit PlaylistFloatGtComparator(const float value) : search_term_(value) {}
|
||||
bool Matches(const QString &element) const override {
|
||||
return element.toFloat() > search_term_;
|
||||
}
|
||||
private:
|
||||
float search_term_;
|
||||
};
|
||||
|
||||
class PlaylistFloatGeComparator : public PlaylistSearchTermComparator {
|
||||
public:
|
||||
explicit PlaylistFloatGeComparator(const float value) : search_term_(value) {}
|
||||
bool Matches(const QString &element) const override {
|
||||
return element.toFloat() >= search_term_;
|
||||
}
|
||||
private:
|
||||
float search_term_;
|
||||
};
|
||||
|
||||
class PlaylistFloatLtComparator : public PlaylistSearchTermComparator {
|
||||
public:
|
||||
explicit PlaylistFloatLtComparator(const float value) : search_term_(value) {}
|
||||
bool Matches(const QString &element) const override {
|
||||
return element.toFloat() < search_term_;
|
||||
}
|
||||
private:
|
||||
float search_term_;
|
||||
};
|
||||
|
||||
class PlaylistFloatLeComparator : public PlaylistSearchTermComparator {
|
||||
public:
|
||||
explicit PlaylistFloatLeComparator(const float value) : search_term_(value) {}
|
||||
bool Matches(const QString &element) const override {
|
||||
return element.toFloat() <= search_term_;
|
||||
}
|
||||
private:
|
||||
float search_term_;
|
||||
};
|
||||
|
||||
class PlaylistGtComparator : public PlaylistSearchTermComparator {
|
||||
public:
|
||||
explicit PlaylistGtComparator(const int value) : search_term_(value) {}
|
||||
bool Matches(const QString &element) const override {
|
||||
return element.toInt() > search_term_;
|
||||
}
|
||||
private:
|
||||
int search_term_;
|
||||
};
|
||||
|
||||
class PlaylistGeComparator : public PlaylistSearchTermComparator {
|
||||
public:
|
||||
explicit PlaylistGeComparator(const int value) : search_term_(value) {}
|
||||
bool Matches(const QString &element) const override {
|
||||
return element.toInt() >= search_term_;
|
||||
}
|
||||
private:
|
||||
int search_term_;
|
||||
};
|
||||
|
||||
class PlaylistLtComparator : public PlaylistSearchTermComparator {
|
||||
public:
|
||||
explicit PlaylistLtComparator(const int value) : search_term_(value) {}
|
||||
bool Matches(const QString &element) const override {
|
||||
return element.toInt() < search_term_;
|
||||
}
|
||||
private:
|
||||
int search_term_;
|
||||
};
|
||||
|
||||
class PlaylistLeComparator : public PlaylistSearchTermComparator {
|
||||
public:
|
||||
explicit PlaylistLeComparator(const int value) : search_term_(value) {}
|
||||
bool Matches(const QString &element) const override {
|
||||
return element.toInt() <= search_term_;
|
||||
}
|
||||
private:
|
||||
int search_term_;
|
||||
};
|
||||
|
||||
// The length field of the playlist (entries) contains a song's running time in nanoseconds.
|
||||
// However, We don't really care about nanoseconds, just seconds.
|
||||
// Thus, with this decorator we drop the last 9 digits, if that many are present.
|
||||
class PlaylistDropTailComparatorDecorator : public PlaylistSearchTermComparator {
|
||||
public:
|
||||
explicit PlaylistDropTailComparatorDecorator(PlaylistSearchTermComparator *cmp) : cmp_(cmp) {}
|
||||
|
||||
bool Matches(const QString &element) const override {
|
||||
if (element.length() > 9) {
|
||||
return cmp_->Matches(element.left(element.length() - 9));
|
||||
}
|
||||
else {
|
||||
return cmp_->Matches(element);
|
||||
}
|
||||
}
|
||||
private:
|
||||
QScopedPointer<PlaylistSearchTermComparator> cmp_;
|
||||
};
|
||||
|
||||
class PlaylistRatingComparatorDecorator : public PlaylistSearchTermComparator {
|
||||
public:
|
||||
explicit PlaylistRatingComparatorDecorator(PlaylistSearchTermComparator *cmp) : cmp_(cmp) {}
|
||||
bool Matches(const QString &element) const override {
|
||||
return cmp_->Matches(QString::number(lround(element.toDouble() * 10.0)));
|
||||
}
|
||||
private:
|
||||
QScopedPointer<PlaylistSearchTermComparator> cmp_;
|
||||
};
|
||||
|
||||
// Filter that applies a SearchTermComparator to all fields of a playlist entry
|
||||
class PlaylistFilterTerm : public PlaylistFilterTree {
|
||||
public:
|
||||
explicit PlaylistFilterTerm(PlaylistSearchTermComparator *comparator, const QList<int> &columns) : cmp_(comparator), columns_(columns) {}
|
||||
|
||||
bool accept(const int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override {
|
||||
for (const int i : columns_) {
|
||||
const QModelIndex idx = model->index(row, i, parent);
|
||||
if (cmp_->Matches(idx.data().toString().toLower())) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
FilterType type() override { return FilterType::Term; }
|
||||
private:
|
||||
QScopedPointer<PlaylistSearchTermComparator> cmp_;
|
||||
QList<int> columns_;
|
||||
};
|
||||
|
||||
// Filter that applies a SearchTermComparator to one specific field of a playlist entry
|
||||
class PlaylistFilterColumnTerm : public PlaylistFilterTree {
|
||||
public:
|
||||
PlaylistFilterColumnTerm(const int column, PlaylistSearchTermComparator *comparator) : col(column), cmp_(comparator) {}
|
||||
|
||||
bool accept(const int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override {
|
||||
const QModelIndex idx = model->index(row, col, parent);
|
||||
return cmp_->Matches(idx.data().toString().toLower());
|
||||
}
|
||||
FilterType type() override { return FilterType::Column; }
|
||||
private:
|
||||
int col;
|
||||
QScopedPointer<PlaylistSearchTermComparator> cmp_;
|
||||
};
|
||||
|
||||
class PlaylistNotFilter : public PlaylistFilterTree {
|
||||
public:
|
||||
explicit PlaylistNotFilter(const PlaylistFilterTree *inv) : child_(inv) {}
|
||||
|
||||
bool accept(const int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override {
|
||||
return !child_->accept(row, parent, model);
|
||||
}
|
||||
FilterType type() override { return FilterType::Not; }
|
||||
private:
|
||||
QScopedPointer<const PlaylistFilterTree> child_;
|
||||
};
|
||||
|
||||
class PlaylistOrFilter : public PlaylistFilterTree {
|
||||
public:
|
||||
~PlaylistOrFilter() override { qDeleteAll(children_); }
|
||||
virtual void add(PlaylistFilterTree *child) { children_.append(child); }
|
||||
bool accept(const int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override {
|
||||
return std::any_of(children_.begin(), children_.end(), [row, parent, model](PlaylistFilterTree *child) { return child->accept(row, parent, model); });
|
||||
}
|
||||
FilterType type() override { return FilterType::Or; }
|
||||
private:
|
||||
QList<PlaylistFilterTree*> children_;
|
||||
};
|
||||
|
||||
class PlaylistAndFilter : public PlaylistFilterTree {
|
||||
public:
|
||||
~PlaylistAndFilter() override { qDeleteAll(children_); }
|
||||
virtual void add(PlaylistFilterTree *child) { children_.append(child); }
|
||||
bool accept(const int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override {
|
||||
return !std::any_of(children_.begin(), children_.end(), [row, parent, model](PlaylistFilterTree *child) { return !child->accept(row, parent, model); });
|
||||
}
|
||||
FilterType type() override { return FilterType::And; }
|
||||
private:
|
||||
QList<PlaylistFilterTree*> children_;
|
||||
};
|
||||
|
||||
PlaylistFilterParser::PlaylistFilterParser(const QString &filter, const QMap<QString, int> &columns, const QSet<int> &numerical_cols) : iter_{}, end_{}, filterstring_(filter), columns_(columns), numerical_columns_(numerical_cols) {}
|
||||
|
||||
PlaylistFilterTree *PlaylistFilterParser::parse() {
|
||||
iter_ = filterstring_.constBegin();
|
||||
end_ = filterstring_.constEnd();
|
||||
return parseOrGroup();
|
||||
}
|
||||
|
||||
void PlaylistFilterParser::advance() {
|
||||
|
||||
while (iter_ != end_ && iter_->isSpace()) {
|
||||
++iter_;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PlaylistFilterTree *PlaylistFilterParser::parseOrGroup() {
|
||||
|
||||
advance();
|
||||
if (iter_ == end_) return new PlaylistNopFilter;
|
||||
|
||||
PlaylistOrFilter *group = new PlaylistOrFilter;
|
||||
group->add(parseAndGroup());
|
||||
advance();
|
||||
while (checkOr()) {
|
||||
group->add(parseAndGroup());
|
||||
advance();
|
||||
}
|
||||
|
||||
return group;
|
||||
|
||||
}
|
||||
|
||||
PlaylistFilterTree *PlaylistFilterParser::parseAndGroup() {
|
||||
|
||||
advance();
|
||||
if (iter_ == end_) return new PlaylistNopFilter;
|
||||
|
||||
PlaylistAndFilter *group = new PlaylistAndFilter();
|
||||
do {
|
||||
group->add(parseSearchExpression());
|
||||
advance();
|
||||
if (iter_ != end_ && *iter_ == QLatin1Char(')')) break;
|
||||
if (checkOr(false)) {
|
||||
break;
|
||||
}
|
||||
checkAnd(); // if there's no 'AND', we'll add the term anyway...
|
||||
} while (iter_ != end_);
|
||||
|
||||
return group;
|
||||
|
||||
}
|
||||
|
||||
bool PlaylistFilterParser::checkAnd() {
|
||||
|
||||
if (iter_ != end_) {
|
||||
if (*iter_ == QLatin1Char('A')) {
|
||||
buf_ += *iter_;
|
||||
++iter_;
|
||||
if (iter_ != end_ && *iter_ == QLatin1Char('N')) {
|
||||
buf_ += *iter_;
|
||||
++iter_;
|
||||
if (iter_ != end_ && *iter_ == QLatin1Char('D')) {
|
||||
buf_ += *iter_;
|
||||
++iter_;
|
||||
if (iter_ != end_ && (iter_->isSpace() || *iter_ == QLatin1Char('-') || *iter_ == QLatin1Char('('))) {
|
||||
advance();
|
||||
buf_.clear();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
bool PlaylistFilterParser::checkOr(const bool step_over) {
|
||||
|
||||
if (!buf_.isEmpty()) {
|
||||
if (buf_ == QLatin1String("OR")) {
|
||||
if (step_over) {
|
||||
buf_.clear();
|
||||
advance();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (iter_ != end_) {
|
||||
if (*iter_ == QLatin1Char('O')) {
|
||||
buf_ += *iter_;
|
||||
++iter_;
|
||||
if (iter_ != end_ && *iter_ == QLatin1Char('R')) {
|
||||
buf_ += *iter_;
|
||||
++iter_;
|
||||
if (iter_ != end_ && (iter_->isSpace() || *iter_ == QLatin1Char('-') || *iter_ == QLatin1Char('('))) {
|
||||
if (step_over) {
|
||||
buf_.clear();
|
||||
advance();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
PlaylistFilterTree *PlaylistFilterParser::parseSearchExpression() {
|
||||
|
||||
advance();
|
||||
if (iter_ == end_) return new PlaylistNopFilter;
|
||||
if (*iter_ == QLatin1Char('(')) {
|
||||
++iter_;
|
||||
advance();
|
||||
PlaylistFilterTree *tree = parseOrGroup();
|
||||
advance();
|
||||
if (iter_ != end_) {
|
||||
if (*iter_ == QLatin1Char(')')) {
|
||||
++iter_;
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
else if (*iter_ == QLatin1Char('-')) {
|
||||
++iter_;
|
||||
PlaylistFilterTree *tree = parseSearchExpression();
|
||||
if (tree->type() != PlaylistFilterTree::FilterType::Nop) return new PlaylistNotFilter(tree);
|
||||
return tree;
|
||||
}
|
||||
else {
|
||||
return parseSearchTerm();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PlaylistFilterTree *PlaylistFilterParser::parseSearchTerm() {
|
||||
|
||||
QString col;
|
||||
QString search;
|
||||
QString prefix;
|
||||
bool inQuotes = false;
|
||||
for (; iter_ != end_; ++iter_) {
|
||||
if (inQuotes) {
|
||||
if (*iter_ == QLatin1Char('"')) {
|
||||
inQuotes = false;
|
||||
}
|
||||
else {
|
||||
buf_ += *iter_;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (*iter_ == QLatin1Char('"')) {
|
||||
inQuotes = true;
|
||||
}
|
||||
else if (col.isEmpty() && *iter_ == QLatin1Char(':')) {
|
||||
col = buf_.toLower();
|
||||
buf_.clear();
|
||||
prefix.clear(); // prefix isn't allowed here - let's ignore it
|
||||
}
|
||||
else if (iter_->isSpace() || *iter_ == QLatin1Char('(') || *iter_ == QLatin1Char(')') || *iter_ == QLatin1Char('-')) {
|
||||
break;
|
||||
}
|
||||
else if (buf_.isEmpty()) {
|
||||
// we don't know whether there is a column part in this search term thus we assume the latter and just try and read a prefix
|
||||
if (prefix.isEmpty() && (*iter_ == QLatin1Char('>') || *iter_ == QLatin1Char('<') || *iter_ == QLatin1Char('=') || *iter_ == QLatin1Char('!'))) {
|
||||
prefix += *iter_;
|
||||
}
|
||||
else if (prefix != QLatin1Char('=') && *iter_ == QLatin1Char('=')) {
|
||||
prefix += *iter_;
|
||||
}
|
||||
else {
|
||||
buf_ += *iter_;
|
||||
}
|
||||
}
|
||||
else {
|
||||
buf_ += *iter_;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
search = buf_.toLower();
|
||||
buf_.clear();
|
||||
|
||||
return createSearchTermTreeNode(col, prefix, search);
|
||||
|
||||
}
|
||||
|
||||
PlaylistFilterTree *PlaylistFilterParser::createSearchTermTreeNode(const QString &col, const QString &prefix, const QString &search) const {
|
||||
|
||||
if (search.isEmpty() && prefix != QLatin1Char('=')) {
|
||||
return new PlaylistNopFilter;
|
||||
}
|
||||
|
||||
PlaylistSearchTermComparator *cmp = nullptr;
|
||||
|
||||
// Handle the float based Rating Column
|
||||
if (columns_[col] == static_cast<int>(Playlist::Column::Rating)) {
|
||||
float parsed_search = Utilities::ParseSearchRating(search);
|
||||
|
||||
if (prefix == QLatin1Char('=')) {
|
||||
cmp = new PlaylistFloatEqComparator(parsed_search);
|
||||
}
|
||||
else if (prefix == QLatin1String("!=") || prefix == QLatin1String("<>")) {
|
||||
cmp = new PlaylistFloatNeComparator(parsed_search);
|
||||
}
|
||||
else if (prefix == QLatin1Char('>')) {
|
||||
cmp = new PlaylistFloatGtComparator(parsed_search);
|
||||
}
|
||||
else if (prefix == QLatin1String(">=")) {
|
||||
cmp = new PlaylistFloatGeComparator(parsed_search);
|
||||
}
|
||||
else if (prefix == QLatin1Char('<')) {
|
||||
cmp = new PlaylistFloatLtComparator(parsed_search);
|
||||
}
|
||||
else if (prefix == QLatin1String("<=")) {
|
||||
cmp = new PlaylistFloatLeComparator(parsed_search);
|
||||
}
|
||||
else {
|
||||
cmp = new PlaylistFloatEqComparator(parsed_search);
|
||||
}
|
||||
}
|
||||
else if (prefix == QLatin1String("!=") || prefix == QLatin1String("<>")) {
|
||||
cmp = new PlaylistNeComparator(search);
|
||||
}
|
||||
else if (!col.isEmpty() && columns_.contains(col) && numerical_columns_.contains(columns_[col])) {
|
||||
// The length column contains the time in seconds (nanoseconds, actually - the "nano" part is handled by the DropTailComparatorDecorator, though).
|
||||
int search_value = 0;
|
||||
if (columns_[col] == static_cast<int>(Playlist::Column::Length)) {
|
||||
search_value = Utilities::ParseSearchTime(search);
|
||||
}
|
||||
else {
|
||||
search_value = search.toInt();
|
||||
}
|
||||
// Alright, back to deciding which comparator we'll use
|
||||
if (prefix == QLatin1Char('>')) {
|
||||
cmp = new PlaylistGtComparator(search_value);
|
||||
}
|
||||
else if (prefix == QLatin1String(">=")) {
|
||||
cmp = new PlaylistGeComparator(search_value);
|
||||
}
|
||||
else if (prefix == QLatin1Char('<')) {
|
||||
cmp = new PlaylistLtComparator(search_value);
|
||||
}
|
||||
else if (prefix == QLatin1String("<=")) {
|
||||
cmp = new PlaylistLeComparator(search_value);
|
||||
}
|
||||
else {
|
||||
// Convert back because for time/rating
|
||||
cmp = new PlaylistEqComparator(QString::number(search_value));
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (prefix == QLatin1Char('=')) {
|
||||
cmp = new PlaylistEqComparator(search);
|
||||
}
|
||||
else if (prefix == QLatin1Char('>')) {
|
||||
cmp = new PlaylistLexicalGtComparator(search);
|
||||
}
|
||||
else if (prefix == QLatin1String(">=")) {
|
||||
cmp = new PlaylistLexicalGeComparator(search);
|
||||
}
|
||||
else if (prefix == QLatin1Char('<')) {
|
||||
cmp = new PlaylistLexicalLtComparator(search);
|
||||
}
|
||||
else if (prefix == QLatin1String("<=")) {
|
||||
cmp = new PlaylistLexicalLeComparator(search);
|
||||
}
|
||||
else {
|
||||
cmp = new PlaylistDefaultComparator(search);
|
||||
}
|
||||
}
|
||||
|
||||
if (columns_.contains(col)) {
|
||||
if (columns_[col] == static_cast<int>(Playlist::Column::Length)) {
|
||||
cmp = new PlaylistDropTailComparatorDecorator(cmp);
|
||||
}
|
||||
return new PlaylistFilterColumnTerm(columns_[col], cmp);
|
||||
}
|
||||
else {
|
||||
return new PlaylistFilterTerm(cmp, columns_.values());
|
||||
}
|
||||
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2019-2023, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2023, Daniel Ostertag <daniel.ostertag@dakes.de>
|
||||
*
|
||||
* Strawberry 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 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include "searchparserutils.h"
|
||||
|
||||
namespace Utilities {
|
||||
|
||||
// Try and parse the string as '[[h:]m:]s' (ignoring all spaces),
|
||||
// and return the number of seconds if it parses correctly.
|
||||
// If not, the original string is returned.
|
||||
// The 'h', 'm' and 's' components can have any length (including 0).
|
||||
// A few examples:
|
||||
// "::" is parsed to "0"
|
||||
// "1::" is parsed to "3600"
|
||||
// "3:45" is parsed to "225"
|
||||
// "1:165" is parsed to "225"
|
||||
// "225" is parsed to "225" (srsly! ^.^)
|
||||
// "2:3:4:5" is parsed to "2:3:4:5"
|
||||
// "25m" is parsed to "25m"
|
||||
|
||||
int ParseSearchTime(const QString &time_str) {
|
||||
|
||||
int seconds = 0;
|
||||
int accum = 0;
|
||||
int colon_count = 0;
|
||||
for (const QChar &c : time_str) {
|
||||
if (c.isDigit()) {
|
||||
accum = accum * 10 + c.digitValue();
|
||||
}
|
||||
else if (c == QLatin1Char(':')) {
|
||||
seconds = seconds * 60 + accum;
|
||||
accum = 0;
|
||||
++colon_count;
|
||||
if (colon_count > 2) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
else if (!c.isSpace()) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
seconds = seconds * 60 + accum;
|
||||
|
||||
return seconds;
|
||||
|
||||
}
|
||||
|
||||
// Parses a rating search term to float.
|
||||
// If the rating is a number from 0-5, map it to 0-1
|
||||
// To use float values directly, the search term can be prefixed with "f" (rating:>f0.2)
|
||||
// If search string is 0, or by default, uses -1
|
||||
// @param rating_str: Rating search 0-5, or "f0.2"
|
||||
// @return float: rating from 0-1 or -1 if not rated.
|
||||
|
||||
float ParseSearchRating(const QString &rating_str) {
|
||||
|
||||
if (rating_str.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
float rating = -1.0F;
|
||||
|
||||
// Check if the search is a float
|
||||
if (rating_str.contains(QLatin1Char('f'), Qt::CaseInsensitive)) {
|
||||
if (rating_str.count(QLatin1Char('f'), Qt::CaseInsensitive) > 1) {
|
||||
return rating;
|
||||
}
|
||||
QString rating_float_str = rating_str;
|
||||
if (rating_str.at(0) == QLatin1Char('f') || rating_str.at(0) == QLatin1Char('F')) {
|
||||
rating_float_str = rating_float_str.remove(0, 1);
|
||||
}
|
||||
if (rating_str.right(1) == QLatin1Char('f') || rating_str.right(1) == QLatin1Char('F')) {
|
||||
rating_float_str.chop(1);
|
||||
}
|
||||
bool ok = false;
|
||||
const float rating_input = rating_float_str.toFloat(&ok);
|
||||
if (ok) {
|
||||
rating = rating_input;
|
||||
}
|
||||
}
|
||||
else {
|
||||
bool ok = false;
|
||||
const int rating_input = rating_str.toInt(&ok);
|
||||
// Is valid int from 0-5: convert to float
|
||||
if (ok && rating_input >= 0 && rating_input <= 5) {
|
||||
rating = static_cast<float>(rating_input) / 5.0F;
|
||||
}
|
||||
}
|
||||
|
||||
// Songs with zero rating have -1 in the DB
|
||||
if (rating == 0) {
|
||||
rating = -1;
|
||||
}
|
||||
|
||||
return rating;
|
||||
|
||||
}
|
||||
|
||||
|
||||
} // namespace Utilities
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2019-2023, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2023, Daniel Ostertag <daniel.ostertag@dakes.de>
|
||||
*
|
||||
* Strawberry 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 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef SEARCHPARSERUTILS_H
|
||||
#define SEARCHPARSERUTILS_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace Utilities {
|
||||
|
||||
int ParseSearchTime(const QString &time_str);
|
||||
float ParseSearchRating(const QString &rating_str);
|
||||
|
||||
} // namespace Utilities
|
||||
|
||||
#endif // SEARCHPARSERUTILS_H
|
Loading…
Reference in New Issue