diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e40c2602..3d8ddc84 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/src/collection/collectionfilter.cpp b/src/collection/collectionfilter.cpp index 9e6de08c..d83194a3 100644 --- a/src/collection/collectionfilter.cpp +++ b/src/collection/collectionfilter.cpp @@ -19,31 +19,17 @@ #include "config.h" -#include - -#include -#include #include -#include #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(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 -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); } diff --git a/src/collection/collectionfilter.h b/src/collection/collectionfilter.h index d3eb3adb..bbe5f801 100644 --- a/src/collection/collectionfilter.h +++ b/src/collection/collectionfilter.h @@ -22,16 +22,10 @@ #include "config.h" -#include -#include #include -#include -#include -#include +#include -#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; - 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 - 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 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 diff --git a/src/collection/collectionfilterwidget.cpp b/src/collection/collectionfilterwidget.cpp index fccfcf0a..c7bb8e56 100644 --- a/src/collection/collectionfilterwidget.cpp +++ b/src/collection/collectionfilterwidget.cpp @@ -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("

") + - tr("Prefix a word with a field name to limit the search to that field, e.g.:") + - QLatin1Char(' ') + - QLatin1String("") + - tr("artist") + - QLatin1String(":Strawbs ") + - tr("searches the collection for all artists that contain the word %1. ").arg(QLatin1String("Strawbs")) + - QLatin1String("

") + - tr("Search terms for numerical fields can be prefixed with %1 or %2 to refine the search, e.g.: ") - .arg(QLatin1String(" =, !=, <, >, <="), QLatin1String(">=")) + - QLatin1String("") + - tr("rating") + - QLatin1String("") + - QLatin1String(":>=") + - QLatin1String("4") + - - QLatin1String("

") + - tr("Available fields") + - QLatin1String(": ") + - QLatin1String("") + - QLatin1String("") + - available_fields + - QLatin1String(".") + - QLatin1String("

") - ); + 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()); } } diff --git a/src/collection/collectionfilterwidget.h b/src/collection/collectionfilterwidget.h index 729571a0..3ae9fa90 100644 --- a/src/collection/collectionfilterwidget.h +++ b/src/collection/collectionfilterwidget.h @@ -128,7 +128,7 @@ class CollectionFilterWidget : public QWidget { QActionGroup *group_by_group_; QHash filter_max_ages_; - QTimer *filter_delay_; + QTimer *timer_filter_delay_; bool filter_applies_to_model_; DelayBehaviour delay_behaviour_; diff --git a/src/collection/collectionquery.cpp b/src/collection/collectionquery.cpp index 067439bb..3599b4c7 100644 --- a/src/collection/collectionquery.cpp +++ b/src/collection/collectionquery.cpp @@ -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), diff --git a/src/filterparser/filterparser.cpp b/src/filterparser/filterparser.cpp new file mode 100644 index 00000000..5854d5fe --- /dev/null +++ b/src/filterparser/filterparser.cpp @@ -0,0 +1,487 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2012, David Sansome + * Copyright 2018-2024, Jonas Kvinge + * Copyright 2023, Daniel Ostertag + * + * 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 . + * + */ + +#include "config.h" + +#include + +#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(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(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("

") + + QObject::tr("Prefix a search term with a field name to limit the search to that field, e.g.:") + + QLatin1Char(' ') + + QLatin1String("") + + QObject::tr("artist") + + QLatin1String(":Strawbs ") + + QObject::tr("searches for all artists containing the word %1. ").arg(QLatin1String("Strawbs")) + + QLatin1String("

") + + + QObject::tr("Search terms for numerical fields can be prefixed with %1 or %2 to refine the search, e.g.: ") + .arg(QLatin1String(" =, !=, <, >, <="), QLatin1String(">=")) + + QLatin1String("") + + QObject::tr("rating") + + QLatin1String("") + + QLatin1String(":>=") + + QLatin1String("4") + + QLatin1String("

") + + + 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("

") + + QObject::tr("Available fields") + + QLatin1String(": ") + QLatin1String("") + + Song::kSearchColumns.join(QLatin1String(", ")) + + QLatin1String(".") + + QLatin1String("

"); + +} diff --git a/src/playlist/playlistfilterparser.h b/src/filterparser/filterparser.h similarity index 52% rename from src/playlist/playlistfilterparser.h rename to src/filterparser/filterparser.h index 6e3319f6..55f0e6b3 100644 --- a/src/playlist/playlistfilterparser.h +++ b/src/filterparser/filterparser.h @@ -3,6 +3,7 @@ * This file was part of Clementine. * Copyright 2012, David Sansome * Copyright 2018-2024, Jonas Kvinge + * Copyright 2023, Daniel Ostertag * * 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 -#include #include -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 &columns, const QSet &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 columns_; - const QSet numerical_columns_; }; -#endif // PLAYLISTFILTERPARSER_H +#endif // FILTERPARSER_H diff --git a/src/filterparser/filterparsersearchcomparators.h b/src/filterparser/filterparsersearchcomparators.h new file mode 100644 index 00000000..f06ef7f7 --- /dev/null +++ b/src/filterparser/filterparsersearchcomparators.h @@ -0,0 +1,324 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2012, David Sansome + * Copyright 2018-2024, Jonas Kvinge + * + * 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 . + * + */ + +#ifndef FILTERPARSERSEARCHCOMPARATORS_H +#define FILTERPARSERSEARCHCOMPARATORS_H + +#include "config.h" + +#include +#include +#include + +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 cmp_; +}; + +#endif // FILTERPARSERSEARCHCOMPARATORS_H diff --git a/src/filterparser/filtertree.cpp b/src/filterparser/filtertree.cpp new file mode 100644 index 00000000..dd824195 --- /dev/null +++ b/src/filterparser/filtertree.cpp @@ -0,0 +1,52 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2012, David Sansome + * Copyright 2018-2024, Jonas Kvinge + * + * 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 . + * + */ + +#include + +#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(); + +} diff --git a/src/filterparser/filtertree.h b/src/filterparser/filtertree.h new file mode 100644 index 00000000..52d52e32 --- /dev/null +++ b/src/filterparser/filtertree.h @@ -0,0 +1,147 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2012, David Sansome + * Copyright 2018-2024, Jonas Kvinge + * + * 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 . + * + */ + +#ifndef FILTERTREE_H +#define FILTERTREE_H + +#include "config.h" + +#include +#include +#include +#include + +#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 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 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 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 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 children_; +}; + +#endif // FILTERTREE_H diff --git a/src/playlist/playlistcontainer.cpp b/src/playlist/playlistcontainer.cpp index 01ea0336..880abb8b 100644 --- a/src/playlist/playlistcontainer.cpp +++ b/src/playlist/playlistcontainer.cpp @@ -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("

") + - tr("Prefix a search term with a field name to limit the search to that field, e.g.:") + - QLatin1Char(' ') + - QLatin1String("") + - tr("artist") + - QLatin1String(":Strawbs ") + - tr("searches the playlist for all artists that contain the word %1. ").arg(QLatin1String("Strawbs")) + - QLatin1String("

") + - - tr("Search terms for numerical fields can be prefixed with %1 or %2 to refine the search, e.g.: ") - .arg(QLatin1String(" =, !=, <, >, <="), QLatin1String(">=")) + - QLatin1String("") + - tr("rating") + - QLatin1String("") + - QLatin1String(":>=") + - QLatin1String("4") + - QLatin1String("

") + - - 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("

") + - tr("Available fields") + - QLatin1String(": ") + QLatin1String("") + - available_fields + - QLatin1String(".") + - QLatin1String("

") - ); - + 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(); diff --git a/src/playlist/playlistfilter.cpp b/src/playlist/playlistfilter.cpp index 00921e0a..0a0a5e3d 100644 --- a/src/playlist/playlistfilter.cpp +++ b/src/playlist/playlistfilter.cpp @@ -23,54 +23,20 @@ #include #include -#include -#include #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(Playlist::Column::Title); - column_names_[QStringLiteral("name")] = static_cast(Playlist::Column::Title); - column_names_[QStringLiteral("artist")] = static_cast(Playlist::Column::Artist); - column_names_[QStringLiteral("album")] = static_cast(Playlist::Column::Album); - column_names_[QStringLiteral("albumartist")] = static_cast(Playlist::Column::AlbumArtist); - column_names_[QStringLiteral("performer")] = static_cast(Playlist::Column::Performer); - column_names_[QStringLiteral("composer")] = static_cast(Playlist::Column::Composer); - column_names_[QStringLiteral("year")] = static_cast(Playlist::Column::Year); - column_names_[QStringLiteral("originalyear")] = static_cast(Playlist::Column::OriginalYear); - column_names_[QStringLiteral("track")] = static_cast(Playlist::Column::Track); - column_names_[QStringLiteral("disc")] = static_cast(Playlist::Column::Disc); - column_names_[QStringLiteral("length")] = static_cast(Playlist::Column::Length); - column_names_[QStringLiteral("genre")] = static_cast(Playlist::Column::Genre); - column_names_[QStringLiteral("samplerate")] = static_cast(Playlist::Column::Samplerate); - column_names_[QStringLiteral("bitdepth")] = static_cast(Playlist::Column::Bitdepth); - column_names_[QStringLiteral("bitrate")] = static_cast(Playlist::Column::Bitrate); - column_names_[QStringLiteral("filename")] = static_cast(Playlist::Column::Filename); - column_names_[QStringLiteral("grouping")] = static_cast(Playlist::Column::Grouping); - column_names_[QStringLiteral("comment")] = static_cast(Playlist::Column::Comment); - column_names_[QStringLiteral("rating")] = static_cast(Playlist::Column::Rating); - column_names_[QStringLiteral("playcount")] = static_cast(Playlist::Column::PlayCount); - column_names_[QStringLiteral("skipcount")] = static_cast(Playlist::Column::SkipCount); - - numerical_columns_ << static_cast(Playlist::Column::Year) - << static_cast(Playlist::Column::OriginalYear) - << static_cast(Playlist::Column::Track) - << static_cast(Playlist::Column::Disc) - << static_cast(Playlist::Column::Length) - << static_cast(Playlist::Column::Samplerate) - << static_cast(Playlist::Column::Bitdepth) - << static_cast(Playlist::Column::Bitrate) - << static_cast(Playlist::Column::PlayCount) - << static_cast(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(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); } diff --git a/src/playlist/playlistfilter.h b/src/playlist/playlistfilter.h index c5a311ed..1312e208 100644 --- a/src/playlist/playlistfilter.h +++ b/src/playlist/playlistfilter.h @@ -24,15 +24,11 @@ #include "config.h" -#include -#include -#include -#include +#include #include #include -#include -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 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 filter_tree_; + mutable QScopedPointer filter_tree_; #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) mutable size_t query_hash_; #else mutable uint query_hash_; #endif - - QMap column_names_; - QSet numerical_columns_; - QString filter_text_; + QString filter_string_; }; #endif // PLAYLISTFILTER_H diff --git a/src/playlist/playlistfilterparser.cpp b/src/playlist/playlistfilterparser.cpp deleted file mode 100644 index 4c6ac03b..00000000 --- a/src/playlist/playlistfilterparser.cpp +++ /dev/null @@ -1,610 +0,0 @@ -/* - * Strawberry Music Player - * This file was part of Clementine. - * Copyright 2012, David Sansome - * Copyright 2018-2024, Jonas Kvinge - * - * 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 . - * - */ - -#include "config.h" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#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 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 cmp_; -}; - -// Filter that applies a SearchTermComparator to all fields of a playlist entry -class PlaylistFilterTerm : public PlaylistFilterTree { - public: - explicit PlaylistFilterTerm(PlaylistSearchTermComparator *comparator, const QList &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 cmp_; - QList 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 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 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 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 children_; -}; - -PlaylistFilterParser::PlaylistFilterParser(const QString &filter, const QMap &columns, const QSet &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(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(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(Playlist::Column::Length)) { - cmp = new PlaylistDropTailComparatorDecorator(cmp); - } - return new PlaylistFilterColumnTerm(columns_[col], cmp); - } - else { - return new PlaylistFilterTerm(cmp, columns_.values()); - } - -} diff --git a/src/utilities/searchparserutils.cpp b/src/utilities/searchparserutils.cpp deleted file mode 100644 index f286eb81..00000000 --- a/src/utilities/searchparserutils.cpp +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2019-2023, Jonas Kvinge - * Copyright 2023, Daniel Ostertag - * - * 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 . - * - */ - -#include - -#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(rating_input) / 5.0F; - } - } - - // Songs with zero rating have -1 in the DB - if (rating == 0) { - rating = -1; - } - - return rating; - -} - - -} // namespace Utilities diff --git a/src/utilities/searchparserutils.h b/src/utilities/searchparserutils.h deleted file mode 100644 index dca6fad2..00000000 --- a/src/utilities/searchparserutils.h +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2019-2023, Jonas Kvinge - * Copyright 2023, Daniel Ostertag - * - * 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 . - * - */ - -#ifndef SEARCHPARSERUTILS_H -#define SEARCHPARSERUTILS_H - -#include - -namespace Utilities { - -int ParseSearchTime(const QString &time_str); -float ParseSearchRating(const QString &rating_str); - -} // namespace Utilities - -#endif // SEARCHPARSERUTILS_H