Add filtering of numerical cols to collection
CollectionFilterWidget: Updated the tooltip, to reflect the changes. CollectionQuery: Add parsing for SQL operators and insert right SQL "where" searches. Song: Add list of numerical columns playlistfilterparser.cpp/FilterParser: move time and rating parsing functions to new file: searchparserutils.cpp: Contains common code used to parse search terms in playlist and collection filters.
This commit is contained in:
parent
82a8a890de
commit
7aa7cdf6f3
|
@ -57,6 +57,7 @@ set(SOURCES
|
||||||
utilities/filemanagerutils.cpp
|
utilities/filemanagerutils.cpp
|
||||||
utilities/coverutils.cpp
|
utilities/coverutils.cpp
|
||||||
utilities/screenutils.cpp
|
utilities/screenutils.cpp
|
||||||
|
utilities/searchparserutils.cpp
|
||||||
|
|
||||||
engine/enginebase.cpp
|
engine/enginebase.cpp
|
||||||
engine/enginedevice.cpp
|
engine/enginedevice.cpp
|
||||||
|
|
|
@ -73,6 +73,7 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
|
||||||
ui_->setupUi(this);
|
ui_->setupUi(this);
|
||||||
|
|
||||||
QString available_fields = Song::kFtsColumns.join(", ").replace(QRegularExpression("\\bfts"), "");
|
QString available_fields = Song::kFtsColumns.join(", ").replace(QRegularExpression("\\bfts"), "");
|
||||||
|
available_fields += QString(", ") + Song::kNumericalColumns.join(", ");
|
||||||
|
|
||||||
ui_->search_field->setToolTip(
|
ui_->search_field->setToolTip(
|
||||||
QString("<html><head/><body><p>") +
|
QString("<html><head/><body><p>") +
|
||||||
|
@ -80,18 +81,26 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
|
||||||
QString(" ") +
|
QString(" ") +
|
||||||
QString("<span style=\"font-weight:600;\">") +
|
QString("<span style=\"font-weight:600;\">") +
|
||||||
tr("artist") +
|
tr("artist") +
|
||||||
QString(":") +
|
QString(":</span><span style=\"font-style:italic;\">Strawbs</span> ") +
|
||||||
QString("</span><span style=\"font-style:italic;\">Strawbs</span>") +
|
tr("searches the collection for all artists that contain the word %1. ").arg("Strawbs") +
|
||||||
QString(" ") +
|
QString("</p><p>") +
|
||||||
tr("searches the collection for all artists that contain the word") +
|
tr("Search terms for numerical fields can be prefixed with %1 or %2 to refine the search, e.g.: ")
|
||||||
QString(" Strawbs.") +
|
.arg(" =, !=, <, >, <=", ">=") +
|
||||||
|
QString("<span style=\"font-weight:600;\">") +
|
||||||
|
tr("rating") +
|
||||||
|
QString("</span>") +
|
||||||
|
QString(":>=") +
|
||||||
|
QString("<span style=\"font-weight:italic;\">4</span>") +
|
||||||
|
|
||||||
QString("</p><p><span style=\"font-weight:600;\">") +
|
QString("</p><p><span style=\"font-weight:600;\">") +
|
||||||
tr("Available fields") +
|
tr("Available fields") +
|
||||||
QString(": ") +
|
QString(": ") +
|
||||||
"</span><span style=\"font-style:italic;\">" +
|
QString("</span>") +
|
||||||
|
QString("<span style=\"font-style:italic;\">") +
|
||||||
available_fields +
|
available_fields +
|
||||||
QString("</span>.") +
|
QString("</span>.") +
|
||||||
QString("</p></body></html>"));
|
QString("</p></body></html>")
|
||||||
|
);
|
||||||
|
|
||||||
QObject::connect(ui_->search_field, &QSearchField::returnPressed, this, &CollectionFilterWidget::ReturnPressed);
|
QObject::connect(ui_->search_field, &QSearchField::returnPressed, this, &CollectionFilterWidget::ReturnPressed);
|
||||||
QObject::connect(filter_delay_, &QTimer::timeout, this, &CollectionFilterWidget::FilterDelayTimeout);
|
QObject::connect(filter_delay_, &QTimer::timeout, this, &CollectionFilterWidget::FilterDelayTimeout);
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
|
|
||||||
#include "collectionquery.h"
|
#include "collectionquery.h"
|
||||||
#include "collectionfilteroptions.h"
|
#include "collectionfilteroptions.h"
|
||||||
|
#include "utilities/searchparserutils.h"
|
||||||
|
|
||||||
CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const QString &fts_table, const CollectionFilterOptions &filter_options)
|
CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const QString &fts_table, const CollectionFilterOptions &filter_options)
|
||||||
: QSqlQuery(db),
|
: QSqlQuery(db),
|
||||||
|
@ -78,6 +79,29 @@ CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_ta
|
||||||
query += "fts" + columntoken + "\"" + subtoken + "\"*";
|
query += "fts" + columntoken + "\"" + subtoken + "\"*";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (Song::kNumericalColumns.contains(token.section(':', 0, 0), Qt::CaseInsensitive)) {
|
||||||
|
// Account for multiple colons.
|
||||||
|
QString columntoken = token.section(':', 0, 0);
|
||||||
|
QString subtoken = token.section(':', 1, -1);
|
||||||
|
subtoken = subtoken.trimmed();
|
||||||
|
if (!subtoken.isEmpty()) {
|
||||||
|
QString comparator = RemoveSqlOperator(subtoken);
|
||||||
|
if (columntoken.compare("rating", Qt::CaseInsensitive) == 0) {
|
||||||
|
subtoken.replace(":", " ");
|
||||||
|
AddWhereRating(subtoken, comparator);
|
||||||
|
}
|
||||||
|
else if (columntoken.compare("length", Qt::CaseInsensitive) == 0) {
|
||||||
|
// time is saved in nanoseconds, so add 9 0's
|
||||||
|
QString parsedTime = QString::number(Utilities::ParseSearchTime(subtoken)) + "000000000";
|
||||||
|
AddWhere(columntoken, parsedTime, comparator);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
subtoken.replace(":", " ");
|
||||||
|
AddWhere(columntoken, subtoken, comparator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// not a valid filter, remove
|
||||||
else {
|
else {
|
||||||
token.replace(":", " ");
|
token.replace(":", " ");
|
||||||
token = token.trimmed();
|
token = token.trimmed();
|
||||||
|
@ -118,6 +142,23 @@ CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_ta
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString CollectionQuery::RemoveSqlOperator(QString &token) {
|
||||||
|
|
||||||
|
QString op = "=";
|
||||||
|
static QRegularExpression rxOp("^(=|<[>=]?|>=?|!=)");
|
||||||
|
QRegularExpressionMatch match = rxOp.match(token);
|
||||||
|
if (match.hasMatch()) {
|
||||||
|
op = match.captured(0);
|
||||||
|
}
|
||||||
|
token.remove(rxOp);
|
||||||
|
|
||||||
|
if (op == "!=") {
|
||||||
|
op = "<>";
|
||||||
|
}
|
||||||
|
return op;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
void CollectionQuery::AddWhere(const QString &column, const QVariant &value, const QString &op) {
|
void CollectionQuery::AddWhere(const QString &column, const QVariant &value, const QString &op) {
|
||||||
|
|
||||||
// Ignore 'literal' for IN
|
// Ignore 'literal' for IN
|
||||||
|
@ -167,6 +208,37 @@ void CollectionQuery::AddWhereArtist(const QVariant &value) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CollectionQuery::AddWhereRating(const QVariant &value, const QString &op) {
|
||||||
|
|
||||||
|
float parsed_rating = Utilities::ParseSearchRating(value.toString());
|
||||||
|
|
||||||
|
// You can't query the database for a float, due to float precision errors,
|
||||||
|
// So we have to use a certain tolerance, so that the searched value is definetly included.
|
||||||
|
const float tolerance = 0.001;
|
||||||
|
if (op == "<") {
|
||||||
|
AddWhere("rating", parsed_rating-tolerance, "<");
|
||||||
|
}
|
||||||
|
else if (op == ">") {
|
||||||
|
AddWhere("rating", parsed_rating+tolerance, ">");
|
||||||
|
}
|
||||||
|
else if (op == "<=") {
|
||||||
|
AddWhere("rating", parsed_rating+tolerance, "<=");
|
||||||
|
}
|
||||||
|
else if (op == ">=") {
|
||||||
|
AddWhere("rating", parsed_rating-tolerance, ">=");
|
||||||
|
}
|
||||||
|
else if (op == "<>") {
|
||||||
|
where_clauses_ << QString("(rating<? OR rating>?)");
|
||||||
|
bound_values_ << parsed_rating - tolerance;
|
||||||
|
bound_values_ << parsed_rating + tolerance;
|
||||||
|
}
|
||||||
|
else /* (op == "=") */ {
|
||||||
|
AddWhere("rating", parsed_rating+tolerance, "<");
|
||||||
|
AddWhere("rating", parsed_rating-tolerance, ">");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
void CollectionQuery::AddCompilationRequirement(const bool compilation) {
|
void CollectionQuery::AddCompilationRequirement(const bool compilation) {
|
||||||
// The unary + is added to prevent sqlite from using the index idx_comp_artist.
|
// The unary + is added to prevent sqlite from using the index idx_comp_artist.
|
||||||
// When joining with fts, sqlite 3.8 has a tendency to use this index and thereby nesting the tables in an order which gives very poor performance
|
// When joining with fts, sqlite 3.8 has a tendency to use this index and thereby nesting the tables in an order which gives very poor performance
|
||||||
|
|
|
@ -61,10 +61,15 @@ class CollectionQuery : public QSqlQuery {
|
||||||
void SetOrderBy(const QString &order_by) { order_by_ = order_by; }
|
void SetOrderBy(const QString &order_by) { order_by_ = order_by; }
|
||||||
|
|
||||||
void SetWhereClauses(const QStringList &where_clauses) { where_clauses_ = where_clauses; }
|
void SetWhereClauses(const QStringList &where_clauses) { where_clauses_ = where_clauses; }
|
||||||
|
|
||||||
|
// Removes = < > <= >= <> from the beginning of the input string and returns the operator
|
||||||
|
// If the input String has no operator, returns "="
|
||||||
|
QString RemoveSqlOperator(QString &token);
|
||||||
// Adds a fragment of WHERE clause. When executed, this Query will connect all the fragments with AND operator.
|
// Adds a fragment of WHERE clause. When executed, this Query will connect all the fragments with AND operator.
|
||||||
// Please note that IN operator expects a QStringList as value.
|
// Please note that IN operator expects a QStringList as value.
|
||||||
void AddWhere(const QString &column, const QVariant &value, const QString &op = "=");
|
void AddWhere(const QString &column, const QVariant &value, const QString &op = "=");
|
||||||
void AddWhereArtist(const QVariant &value);
|
void AddWhereArtist(const QVariant &value);
|
||||||
|
void AddWhereRating(const QVariant &value, const QString &op = "=");
|
||||||
|
|
||||||
void SetBoundValues(const QVariantList &bound_values) { bound_values_ = bound_values; }
|
void SetBoundValues(const QVariantList &bound_values) { bound_values_ = bound_values; }
|
||||||
void SetDuplicatesOnly(const bool duplicates_only) { duplicates_only_ = duplicates_only; }
|
void SetDuplicatesOnly(const bool duplicates_only) { duplicates_only_ = duplicates_only; }
|
||||||
|
|
|
@ -142,6 +142,17 @@ const QString Song::kColumnSpec = Song::kColumns.join(", ");
|
||||||
const QString Song::kBindSpec = Utilities::Prepend(":", Song::kColumns).join(", ");
|
const QString Song::kBindSpec = Utilities::Prepend(":", Song::kColumns).join(", ");
|
||||||
const QString Song::kUpdateSpec = Utilities::Updateify(Song::kColumns).join(", ");
|
const QString Song::kUpdateSpec = Utilities::Updateify(Song::kColumns).join(", ");
|
||||||
|
|
||||||
|
// used to indicate, what columns can be filtered numerically. Used by the CollectionQuery.
|
||||||
|
const QStringList Song::kNumericalColumns = QStringList() << "year"
|
||||||
|
<< "length"
|
||||||
|
<< "samplerate"
|
||||||
|
<< "bitdepth"
|
||||||
|
<< "bitrate"
|
||||||
|
<< "rating"
|
||||||
|
<< "playcount"
|
||||||
|
<< "skipcount";
|
||||||
|
|
||||||
|
|
||||||
const QStringList Song::kFtsColumns = QStringList() << "ftstitle"
|
const QStringList Song::kFtsColumns = QStringList() << "ftstitle"
|
||||||
<< "ftsalbum"
|
<< "ftsalbum"
|
||||||
<< "ftsartist"
|
<< "ftsartist"
|
||||||
|
|
|
@ -116,6 +116,8 @@ class Song {
|
||||||
static const QString kBindSpec;
|
static const QString kBindSpec;
|
||||||
static const QString kUpdateSpec;
|
static const QString kUpdateSpec;
|
||||||
|
|
||||||
|
static const QStringList kNumericalColumns;
|
||||||
|
|
||||||
static const QStringList kFtsColumns;
|
static const QStringList kFtsColumns;
|
||||||
static const QString kFtsColumnSpec;
|
static const QString kFtsColumnSpec;
|
||||||
static const QString kFtsBindSpec;
|
static const QString kFtsBindSpec;
|
||||||
|
|
|
@ -123,7 +123,7 @@ PlaylistContainer::PlaylistContainer(QWidget *parent)
|
||||||
QObject::connect(ui_->playlist, &PlaylistView::FocusOnFilterSignal, this, &PlaylistContainer::FocusOnFilter);
|
QObject::connect(ui_->playlist, &PlaylistView::FocusOnFilterSignal, this, &PlaylistContainer::FocusOnFilter);
|
||||||
ui_->search_field->installEventFilter(this);
|
ui_->search_field->installEventFilter(this);
|
||||||
|
|
||||||
QString available_fields = PlaylistFilter().column_names_.keys().join(", ");
|
QString available_fields = PlaylistFilter().column_names().keys().join(", ");
|
||||||
ui_->search_field->setToolTip(
|
ui_->search_field->setToolTip(
|
||||||
QString("<html><head/><body><p>") +
|
QString("<html><head/><body><p>") +
|
||||||
tr("Prefix a search term with a field name to limit the search to that field, e.g.:") +
|
tr("Prefix a search term with a field name to limit the search to that field, e.g.:") +
|
||||||
|
@ -131,7 +131,7 @@ PlaylistContainer::PlaylistContainer(QWidget *parent)
|
||||||
QString("<span style=\"font-weight:600;\">") +
|
QString("<span style=\"font-weight:600;\">") +
|
||||||
tr("artist") +
|
tr("artist") +
|
||||||
QString(":</span><span style=\"font-style:italic;\">Strawbs</span> ") +
|
QString(":</span><span style=\"font-style:italic;\">Strawbs</span> ") +
|
||||||
tr("searches the collection for all artists that contain the word %1. ").arg("Strawbs") +
|
tr("searches the playlist for all artists that contain the word %1. ").arg("Strawbs") +
|
||||||
QString("</p><p>") +
|
QString("</p><p>") +
|
||||||
|
|
||||||
tr("Search terms for numerical fields can be prefixed with %1 or %2 to refine the search, e.g.: ")
|
tr("Search terms for numerical fields can be prefixed with %1 or %2 to refine the search, e.g.: ")
|
||||||
|
|
|
@ -51,8 +51,7 @@ class PlaylistFilter : public QSortFilterProxyModel {
|
||||||
void SetFilterText(const QString &filter_text);
|
void SetFilterText(const QString &filter_text);
|
||||||
|
|
||||||
QString filter_text() const { return filter_text_; }
|
QString filter_text() const { return filter_text_; }
|
||||||
|
QMap<QString, int> column_names() const { return column_names_; }
|
||||||
QMap<QString, int> column_names_;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Mutable because they're modified from filterAcceptsRow() const
|
// Mutable because they're modified from filterAcceptsRow() const
|
||||||
|
@ -63,6 +62,7 @@ class PlaylistFilter : public QSortFilterProxyModel {
|
||||||
mutable uint query_hash_;
|
mutable uint query_hash_;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
QMap<QString, int> column_names_;
|
||||||
QSet<int> numerical_columns_;
|
QSet<int> numerical_columns_;
|
||||||
QString filter_text_;
|
QString filter_text_;
|
||||||
};
|
};
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
|
|
||||||
#include "playlist.h"
|
#include "playlist.h"
|
||||||
#include "playlistfilterparser.h"
|
#include "playlistfilterparser.h"
|
||||||
|
#include "utilities/searchparserutils.h"
|
||||||
|
|
||||||
class SearchTermComparator {
|
class SearchTermComparator {
|
||||||
public:
|
public:
|
||||||
|
@ -518,28 +519,28 @@ FilterTree *FilterParser::createSearchTermTreeNode(const QString &col, const QSt
|
||||||
|
|
||||||
// Handle the float based Rating Column
|
// Handle the float based Rating Column
|
||||||
if (columns_[col] == Playlist::Column_Rating) {
|
if (columns_[col] == Playlist::Column_Rating) {
|
||||||
float parsedSearch = parseRating(search);
|
float parsed_search = Utilities::ParseSearchRating(search);
|
||||||
|
|
||||||
if (prefix == "=") {
|
if (prefix == "=") {
|
||||||
cmp = new FloatEqComparator(parsedSearch);
|
cmp = new FloatEqComparator(parsed_search);
|
||||||
}
|
}
|
||||||
else if (prefix == "!=" || prefix == "<>") {
|
else if (prefix == "!=" || prefix == "<>") {
|
||||||
cmp = new FloatNeComparator(parsedSearch);
|
cmp = new FloatNeComparator(parsed_search);
|
||||||
}
|
}
|
||||||
else if (prefix == ">") {
|
else if (prefix == ">") {
|
||||||
cmp = new FloatGtComparator(parsedSearch);
|
cmp = new FloatGtComparator(parsed_search);
|
||||||
}
|
}
|
||||||
else if (prefix == ">=") {
|
else if (prefix == ">=") {
|
||||||
cmp = new FloatGeComparator(parsedSearch);
|
cmp = new FloatGeComparator(parsed_search);
|
||||||
}
|
}
|
||||||
else if (prefix == "<") {
|
else if (prefix == "<") {
|
||||||
cmp = new FloatLtComparator(parsedSearch);
|
cmp = new FloatLtComparator(parsed_search);
|
||||||
}
|
}
|
||||||
else if (prefix == "<=") {
|
else if (prefix == "<=") {
|
||||||
cmp = new FloatLeComparator(parsedSearch);
|
cmp = new FloatLeComparator(parsed_search);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
cmp = new FloatEqComparator(parsedSearch);
|
cmp = new FloatEqComparator(parsed_search);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (prefix == "!=" || prefix == "<>") {
|
else if (prefix == "!=" || prefix == "<>") {
|
||||||
|
@ -549,7 +550,7 @@ FilterTree *FilterParser::createSearchTermTreeNode(const QString &col, const QSt
|
||||||
// the length column contains the time in seconds (nanoseconds, actually - the "nano" part is handled by the DropTailComparatorDecorator, though).
|
// the length column contains the time in seconds (nanoseconds, actually - the "nano" part is handled by the DropTailComparatorDecorator, though).
|
||||||
int search_value = 0;
|
int search_value = 0;
|
||||||
if (columns_[col] == Playlist::Column_Length) {
|
if (columns_[col] == Playlist::Column_Length) {
|
||||||
search_value = parseTime(search);
|
search_value = Utilities::ParseSearchTime(search);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
search_value = search.toInt();
|
search_value = search.toInt();
|
||||||
|
@ -604,80 +605,3 @@ FilterTree *FilterParser::createSearchTermTreeNode(const QString &col, const QSt
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// The rating column contains the rating as a float from 0-1 or -1 if unrated.
|
|
||||||
// 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 is 0, or by default, uses -1
|
|
||||||
float FilterParser::parseRating(const QString &rating_str) {
|
|
||||||
|
|
||||||
if (rating_str.isEmpty()) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
float rating = -1;
|
|
||||||
bool ok = false;
|
|
||||||
float rating_input = rating_str.toFloat(&ok);
|
|
||||||
// is valid int from 0-5: convert to float
|
|
||||||
if (ok && rating_input >= 0 && rating_input <= 5) {
|
|
||||||
rating = rating_input / 5.0F;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the search is a float
|
|
||||||
else if (rating_str.at(0) == 'f') {
|
|
||||||
QString rating_float = rating_str;
|
|
||||||
rating_float = rating_float.remove(0, 1);
|
|
||||||
|
|
||||||
ok = false;
|
|
||||||
rating_float.toFloat(&ok);
|
|
||||||
if (ok) {
|
|
||||||
rating = rating_float.toFloat(&ok);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Songs with zero rating have -1 in the DB
|
|
||||||
if (rating == 0) {
|
|
||||||
rating = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return rating;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -88,8 +88,6 @@ class FilterParser {
|
||||||
FilterTree *parseSearchTerm();
|
FilterTree *parseSearchTerm();
|
||||||
|
|
||||||
FilterTree *createSearchTermTreeNode(const QString &col, const QString &prefix, const QString &search) const;
|
FilterTree *createSearchTermTreeNode(const QString &col, const QString &prefix, const QString &search) const;
|
||||||
static int parseTime(const QString &time_str);
|
|
||||||
static float parseRating(const QString &rating_str);
|
|
||||||
|
|
||||||
QString::const_iterator iter_;
|
QString::const_iterator iter_;
|
||||||
QString::const_iterator end_;
|
QString::const_iterator end_;
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
/*
|
||||||
|
* Strawberry Music Player
|
||||||
|
* Copyright 2019-2023, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
|
* Copyright 2023, Daniel Ostertag <daniel.ostertag@dakes.de>
|
||||||
|
*
|
||||||
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Strawberry is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "searchparserutils.h"
|
||||||
|
|
||||||
|
namespace Utilities {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 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"
|
||||||
|
* @param time_str
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
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 == ':') {
|
||||||
|
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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 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 str 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;
|
||||||
|
bool ok = false;
|
||||||
|
float rating_input = rating_str.toFloat(&ok);
|
||||||
|
// is valid int from 0-5: convert to float
|
||||||
|
if (ok && rating_input >= 0 && rating_input <= 5) {
|
||||||
|
rating = rating_input / 5.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the search is a float
|
||||||
|
else if (rating_str.at(0) == 'f') {
|
||||||
|
QString rating_float = rating_str;
|
||||||
|
rating_float = rating_float.remove(0, 1);
|
||||||
|
|
||||||
|
ok = false;
|
||||||
|
rating_float.toFloat(&ok);
|
||||||
|
if (ok) {
|
||||||
|
rating = rating_float.toFloat(&ok);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Songs with zero rating have -1 in the DB
|
||||||
|
if (rating == 0) {
|
||||||
|
rating = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rating;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} // namespace Utilities
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Strawberry Music Player
|
||||||
|
* Copyright 2019-2023, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
|
* Copyright 2023, Daniel Ostertag <daniel.ostertag@dakes.de>
|
||||||
|
*
|
||||||
|
* Strawberry is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Strawberry is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SEARCHPARSERUTILS_H
|
||||||
|
#define SEARCHPARSERUTILS_H
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace Utilities {
|
||||||
|
|
||||||
|
int ParseSearchTime(const QString &time_str);
|
||||||
|
float ParseSearchRating(const QString &rating_str);
|
||||||
|
|
||||||
|
} // namespace Utilities
|
||||||
|
|
||||||
|
#endif // SEARCHPARSERUTILS_H
|
Loading…
Reference in New Issue