/* * Strawberry Music Player * This file was part of Clementine. * Copyright 2012, David Sansome * * 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 "playlist.h" #include "playlistfilterparser.h" class SearchTermComparator { public: virtual ~SearchTermComparator() {} virtual bool Matches(const QString &element) const = 0; }; // "compares" by checking if the field contains the search term class DefaultComparator : public SearchTermComparator { public: explicit DefaultComparator(const QString &value) : search_term_(value) {} bool Matches(const QString &element) const override { return element.contains(search_term_); } private: QString search_term_; }; class EqComparator : public SearchTermComparator { public: explicit EqComparator(const QString &value) : search_term_(value) {} bool Matches(const QString &element) const override { return search_term_ == element; } private: QString search_term_; }; class NeComparator : public SearchTermComparator { public: explicit NeComparator(const QString &value) : search_term_(value) {} bool Matches(const QString &element) const override { return search_term_ != element; } private: QString search_term_; }; class LexicalGtComparator : public SearchTermComparator { public: explicit LexicalGtComparator(const QString &value) : search_term_(value) {} bool Matches(const QString &element) const override { return element > search_term_; } private: QString search_term_; }; class LexicalGeComparator : public SearchTermComparator { public: explicit LexicalGeComparator(const QString &value) : search_term_(value) {} bool Matches(const QString &element) const override { return element >= search_term_; } private: QString search_term_; }; class LexicalLtComparator : public SearchTermComparator { public: explicit LexicalLtComparator(const QString &value) : search_term_(value) {} bool Matches(const QString &element) const override { return element < search_term_; } private: QString search_term_; }; class LexicalLeComparator : public SearchTermComparator { public: explicit LexicalLeComparator(const QString &value) : search_term_(value) {} bool Matches(const QString &element) const override { return element <= search_term_; } private: QString search_term_; }; class GtComparator : public SearchTermComparator { public: explicit GtComparator(int value) : search_term_(value) {} bool Matches(const QString &element) const override { return element.toInt() > search_term_; } private: int search_term_; }; class GeComparator : public SearchTermComparator { public: explicit GeComparator(int value) : search_term_(value) {} bool Matches(const QString &element) const override { return element.toInt() >= search_term_; } private: int search_term_; }; class LtComparator : public SearchTermComparator { public: explicit LtComparator(int value) : search_term_(value) {} bool Matches(const QString &element) const override { return element.toInt() < search_term_; } private: int search_term_; }; class LeComparator : public SearchTermComparator { public: explicit LeComparator(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 nano seconds. // However, We don't really care about nano seconds, just seconds. // Thus, with this decorator we drop the last 9 digits, if that many are present. class DropTailComparatorDecorator : public SearchTermComparator { public: explicit DropTailComparatorDecorator(SearchTermComparator *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 RatingComparatorDecorator : public SearchTermComparator { public: explicit RatingComparatorDecorator(SearchTermComparator *cmp) : cmp_(cmp) {} bool Matches(const QString &element) const override { return cmp_->Matches(QString::number(static_cast(element.toDouble() * 10.0 + 0.5))); } private: QScopedPointer cmp_; }; // filter that applies a SearchTermComparator to all fields of a playlist entry class FilterTerm : public FilterTree { public: explicit FilterTerm(SearchTermComparator *comparator, const QList &columns) : cmp_(comparator), columns_(columns) {} bool accept(int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override { for (int i : columns_) { QModelIndex idx(model->index(row, i, parent)); if (cmp_->Matches(idx.data().toString().toLower())) return true; } return false; } FilterType type() override { return Term; } private: QScopedPointer cmp_; QList columns_; }; // filter that applies a SearchTermComparator to one specific field of a playlist entry class FilterColumnTerm : public FilterTree { public: FilterColumnTerm(int column, SearchTermComparator *comparator) : col(column), cmp_(comparator) {} bool accept(int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override { QModelIndex idx(model->index(row, col, parent)); return cmp_->Matches(idx.data().toString().toLower()); } FilterType type() override { return Column; } private: int col; QScopedPointer cmp_; }; class NotFilter : public FilterTree { public: explicit NotFilter(const FilterTree *inv) : child_(inv) {} bool accept(int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override { return !child_->accept(row, parent, model); } FilterType type() override { return Not; } private: QScopedPointer child_; }; class OrFilter : public FilterTree { public: ~OrFilter() override { qDeleteAll(children_); } virtual void add(FilterTree *child) { children_.append(child); } bool accept(int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override { for (FilterTree *child : children_) { if (child->accept(row, parent, model)) return true; } return false; } FilterType type() override { return Or; } private: QList children_; }; class AndFilter : public FilterTree { public: ~AndFilter() override { qDeleteAll(children_); } virtual void add(FilterTree *child) { children_.append(child); } bool accept(int row, const QModelIndex &parent, const QAbstractItemModel *const model) const override { for (FilterTree *child : children_) { if (!child->accept(row, parent, model)) return false; } return true; } FilterType type() override { return And; } private: QList children_; }; FilterParser::FilterParser(const QString &filter, const QMap &columns, const QSet &numerical_cols) : filterstring_(filter), columns_(columns), numerical_columns_(numerical_cols) {} FilterTree *FilterParser::parse() { iter_ = filterstring_.constBegin(); end_ = filterstring_.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_ == QChar(')')) 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_ == QChar('A')) { buf_ += *iter_; ++iter_; if (iter_ != end_ && *iter_ == QChar('N')) { buf_ += *iter_; ++iter_; if (iter_ != end_ && *iter_ == QChar('D')) { buf_ += *iter_; ++iter_; if (iter_ != end_ && (iter_->isSpace() || *iter_ == QChar('-') || *iter_ == '(')) { advance(); buf_.clear(); return true; } } } } } return false; } bool FilterParser::checkOr(bool step_over) { if (!buf_.isEmpty()) { if (buf_ == "OR") { if (step_over) { buf_.clear(); advance(); } return true; } } else { if (iter_ != end_) { if (*iter_ == 'O') { buf_ += *iter_; ++iter_; if (iter_ != end_ && *iter_ == 'R') { buf_ += *iter_; ++iter_; if (iter_ != end_ && (iter_->isSpace() || *iter_ == '-' || *iter_ == '(')) { if (step_over) { buf_.clear(); advance(); } return true; } } } } } return false; } FilterTree *FilterParser::parseSearchExpression() { advance(); if (iter_ == end_) return new NopFilter; if (*iter_ == '(') { ++iter_; advance(); FilterTree *tree = parseOrGroup(); advance(); if (iter_ != end_) { if (*iter_ == ')') { ++iter_; } } return tree; } else if (*iter_ == '-') { ++iter_; FilterTree *tree = parseSearchExpression(); if (tree->type() != FilterTree::Nop) return new NotFilter(tree); return tree; } else { return parseSearchTerm(); } } FilterTree *FilterParser::parseSearchTerm() { QString col; QString search; QString prefix; bool inQuotes = false; for (; iter_ != end_; ++iter_) { if (inQuotes) { if (*iter_ == '"') inQuotes = false; else buf_ += *iter_; } else { if (*iter_ == '"') { inQuotes = true; } else if (col.isEmpty() && *iter_ == ':') { col = buf_.toLower(); buf_.clear(); prefix.clear(); // prefix isn't allowed here - let's ignore it } else if (iter_->isSpace() || *iter_ == '(' || *iter_ == ')' || *iter_ == '-') { 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_ == '>' || *iter_ == '<' || *iter_ == '=' || *iter_ == '!')) { prefix += *iter_; } else if (prefix != "=" && *iter_ == '=') { prefix += *iter_; } else { buf_ += *iter_; } } else { buf_ += *iter_; } } } search = buf_.toLower(); buf_.clear(); return createSearchTermTreeNode(col, prefix, search); } FilterTree *FilterParser::createSearchTermTreeNode(const QString &col, const QString &prefix, const QString &search) const { if (search.isEmpty() && prefix != "=") { return new NopFilter; } // here comes a mess :/ // well, not that much of a mess, but so many options -_- SearchTermComparator *cmp = nullptr; if (prefix == "!=" || prefix == "<>") { cmp = new NeComparator(search); } else if (!col.isEmpty() && columns_.contains(col) && numerical_columns_.contains(columns_[col])) { // the length column contains the time in seconds (nano seconds, actually - the "nano" part is handled by the DropTailComparatorDecorator, though). int search_value = 0; if (columns_[col] == Playlist::Column_Length) { search_value = parseTime(search); } else { search_value = search.toInt(); } // alright, back to deciding which comparator we'll use if (prefix == ">") { cmp = new GtComparator(search_value); } else if (prefix == ">=") { cmp = new GeComparator(search_value); } else if (prefix == "<") { cmp = new LtComparator(search_value); } else if (prefix == "<=") { cmp = new LeComparator(search_value); } else { // convert back because for time/rating cmp = new EqComparator(QString::number(search_value)); } } else { if (prefix == "=") { cmp = new EqComparator(search); } else if (prefix == ">") { cmp = new LexicalGtComparator(search); } else if (prefix == ">=") { cmp = new LexicalGeComparator(search); } else if (prefix == "<") { cmp = new LexicalLtComparator(search); } else if (prefix == "<=") { cmp = new LexicalLeComparator(search); } else { cmp = new DefaultComparator(search); } } if (columns_.contains(col)) { if (columns_[col] == Playlist::Column_Length) { cmp = new DropTailComparatorDecorator(cmp); } return new FilterColumnTerm(columns_[col], cmp); } else { return new FilterTerm(cmp, columns_.values()); } } // 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 FilterParser::parseTime(const QString &time_str) const { 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 == ':') { 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; }