mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 16:42:42 +01:00
Merge pull request #4251 from rcrowell/query_prefixes
Add query prefixes :~ and :=
This commit is contained in:
commit
19e4f41a72
5 changed files with 109 additions and 4 deletions
|
|
@ -177,6 +177,23 @@ class StringFieldQuery(FieldQuery):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class StringQuery(StringFieldQuery):
|
||||||
|
"""A query that matches a whole string in a specific item field."""
|
||||||
|
|
||||||
|
def col_clause(self):
|
||||||
|
search = (self.pattern
|
||||||
|
.replace('\\', '\\\\')
|
||||||
|
.replace('%', '\\%')
|
||||||
|
.replace('_', '\\_'))
|
||||||
|
clause = self.field + " like ? escape '\\'"
|
||||||
|
subvals = [search]
|
||||||
|
return clause, subvals
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def string_match(cls, pattern, value):
|
||||||
|
return pattern.lower() == value.lower()
|
||||||
|
|
||||||
|
|
||||||
class SubstringQuery(StringFieldQuery):
|
class SubstringQuery(StringFieldQuery):
|
||||||
"""A query that matches a substring in a specific item field."""
|
"""A query that matches a substring in a specific item field."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1385,7 +1385,11 @@ def parse_query_parts(parts, model_cls):
|
||||||
special path query detection.
|
special path query detection.
|
||||||
"""
|
"""
|
||||||
# Get query types and their prefix characters.
|
# Get query types and their prefix characters.
|
||||||
prefixes = {':': dbcore.query.RegexpQuery}
|
prefixes = {
|
||||||
|
':': dbcore.query.RegexpQuery,
|
||||||
|
'~': dbcore.query.StringQuery,
|
||||||
|
'=': dbcore.query.MatchQuery,
|
||||||
|
}
|
||||||
prefixes.update(plugins.queries())
|
prefixes.update(plugins.queries())
|
||||||
|
|
||||||
# Special-case path-like queries, which are non-field queries
|
# Special-case path-like queries, which are non-field queries
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ New features:
|
||||||
* :doc:`/plugins/kodiupdate`: Now supports multiple kodi instances
|
* :doc:`/plugins/kodiupdate`: Now supports multiple kodi instances
|
||||||
:bug:`4101`
|
:bug:`4101`
|
||||||
* Add the item fields ``bitrate_mode``, ``encoder_info`` and ``encoder_settings``.
|
* Add the item fields ``bitrate_mode``, ``encoder_info`` and ``encoder_settings``.
|
||||||
|
* Add query prefixes ``=`` and ``~``.
|
||||||
|
|
||||||
Bug fixes:
|
Bug fixes:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,14 +93,45 @@ backslashes are not part of beets' syntax; I'm just using the escaping
|
||||||
functionality of my shell (bash or zsh, for instance) to pass ``the rebel`` as a
|
functionality of my shell (bash or zsh, for instance) to pass ``the rebel`` as a
|
||||||
single argument instead of two.
|
single argument instead of two.
|
||||||
|
|
||||||
|
Exact Matches
|
||||||
|
-------------
|
||||||
|
|
||||||
|
While ordinary queries perform *substring* matches, beets can also match whole
|
||||||
|
strings by adding either ``=`` (case-sensitive) or ``~`` (ignore case) after the
|
||||||
|
field name's colon and before the expression::
|
||||||
|
|
||||||
|
$ beet list artist:air
|
||||||
|
$ beet list artist:~air
|
||||||
|
$ beet list artist:=AIR
|
||||||
|
|
||||||
|
The first query is a simple substring one that returns tracks by Air, AIR, and
|
||||||
|
Air Supply. The second query returns tracks by Air and AIR, since both are a
|
||||||
|
case-insensitive match for the entire expression, but does not return anything
|
||||||
|
by Air Supply. The third query, which requires a case-sensitive exact match,
|
||||||
|
returns tracks by AIR only.
|
||||||
|
|
||||||
|
Exact matches may be performed on phrases as well::
|
||||||
|
|
||||||
|
$ beet list artist:~"dave matthews"
|
||||||
|
$ beet list artist:="Dave Matthews"
|
||||||
|
|
||||||
|
Both of these queries return tracks by Dave Matthews, but not by Dave Matthews
|
||||||
|
Band.
|
||||||
|
|
||||||
|
To search for exact matches across *all* fields, just prefix the expression with
|
||||||
|
a single ``=`` or ``~``::
|
||||||
|
|
||||||
|
$ beet list ~crash
|
||||||
|
$ beet list ="American Football"
|
||||||
|
|
||||||
.. _regex:
|
.. _regex:
|
||||||
|
|
||||||
Regular Expressions
|
Regular Expressions
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
While ordinary keywords perform simple substring matches, beets also supports
|
In addition to simple substring and exact matches, beets also supports regular
|
||||||
regular expression matching for more advanced queries. To run a regex query, use
|
expression matching for more advanced queries. To run a regex query, use an
|
||||||
an additional ``:`` between the field name and the expression::
|
additional ``:`` between the field name and the expression::
|
||||||
|
|
||||||
$ beet list "artist::Ann(a|ie)"
|
$ beet list "artist::Ann(a|ie)"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,16 +94,19 @@ class DummyDataTestCase(_common.TestCase, AssertsMixin):
|
||||||
items[0].album = 'baz'
|
items[0].album = 'baz'
|
||||||
items[0].year = 2001
|
items[0].year = 2001
|
||||||
items[0].comp = True
|
items[0].comp = True
|
||||||
|
items[0].genre = 'rock'
|
||||||
items[1].title = 'baz qux'
|
items[1].title = 'baz qux'
|
||||||
items[1].artist = 'two'
|
items[1].artist = 'two'
|
||||||
items[1].album = 'baz'
|
items[1].album = 'baz'
|
||||||
items[1].year = 2002
|
items[1].year = 2002
|
||||||
items[1].comp = True
|
items[1].comp = True
|
||||||
|
items[1].genre = 'Rock'
|
||||||
items[2].title = 'beets 4 eva'
|
items[2].title = 'beets 4 eva'
|
||||||
items[2].artist = 'three'
|
items[2].artist = 'three'
|
||||||
items[2].album = 'foo'
|
items[2].album = 'foo'
|
||||||
items[2].year = 2003
|
items[2].year = 2003
|
||||||
items[2].comp = False
|
items[2].comp = False
|
||||||
|
items[2].genre = 'Hard Rock'
|
||||||
for item in items:
|
for item in items:
|
||||||
self.lib.add(item)
|
self.lib.add(item)
|
||||||
self.album = self.lib.add_album(items[:2])
|
self.album = self.lib.add_album(items[:2])
|
||||||
|
|
@ -132,6 +135,22 @@ class GetTest(DummyDataTestCase):
|
||||||
results = self.lib.items(q)
|
results = self.lib.items(q)
|
||||||
self.assert_items_matched(results, ['baz qux'])
|
self.assert_items_matched(results, ['baz qux'])
|
||||||
|
|
||||||
|
def test_get_one_keyed_exact(self):
|
||||||
|
q = 'genre:=rock'
|
||||||
|
results = self.lib.items(q)
|
||||||
|
self.assert_items_matched(results, ['foo bar'])
|
||||||
|
q = 'genre:=Rock'
|
||||||
|
results = self.lib.items(q)
|
||||||
|
self.assert_items_matched(results, ['baz qux'])
|
||||||
|
q = 'genre:="Hard Rock"'
|
||||||
|
results = self.lib.items(q)
|
||||||
|
self.assert_items_matched(results, ['beets 4 eva'])
|
||||||
|
|
||||||
|
def test_get_one_keyed_exact_nocase(self):
|
||||||
|
q = 'genre:~"hard rock"'
|
||||||
|
results = self.lib.items(q)
|
||||||
|
self.assert_items_matched(results, ['beets 4 eva'])
|
||||||
|
|
||||||
def test_get_one_keyed_regexp(self):
|
def test_get_one_keyed_regexp(self):
|
||||||
q = 'artist::t.+r'
|
q = 'artist::t.+r'
|
||||||
results = self.lib.items(q)
|
results = self.lib.items(q)
|
||||||
|
|
@ -142,6 +161,16 @@ class GetTest(DummyDataTestCase):
|
||||||
results = self.lib.items(q)
|
results = self.lib.items(q)
|
||||||
self.assert_items_matched(results, ['beets 4 eva'])
|
self.assert_items_matched(results, ['beets 4 eva'])
|
||||||
|
|
||||||
|
def test_get_one_unkeyed_exact(self):
|
||||||
|
q = '=rock'
|
||||||
|
results = self.lib.items(q)
|
||||||
|
self.assert_items_matched(results, ['foo bar'])
|
||||||
|
|
||||||
|
def test_get_one_unkeyed_exact_nocase(self):
|
||||||
|
q = '~"hard rock"'
|
||||||
|
results = self.lib.items(q)
|
||||||
|
self.assert_items_matched(results, ['beets 4 eva'])
|
||||||
|
|
||||||
def test_get_one_unkeyed_regexp(self):
|
def test_get_one_unkeyed_regexp(self):
|
||||||
q = ':x$'
|
q = ':x$'
|
||||||
results = self.lib.items(q)
|
results = self.lib.items(q)
|
||||||
|
|
@ -159,6 +188,11 @@ class GetTest(DummyDataTestCase):
|
||||||
# objects.
|
# objects.
|
||||||
self.assert_items_matched(results, [])
|
self.assert_items_matched(results, [])
|
||||||
|
|
||||||
|
def test_get_no_matches_exact(self):
|
||||||
|
q = 'genre:="hard rock"'
|
||||||
|
results = self.lib.items(q)
|
||||||
|
self.assert_items_matched(results, [])
|
||||||
|
|
||||||
def test_term_case_insensitive(self):
|
def test_term_case_insensitive(self):
|
||||||
q = 'oNE'
|
q = 'oNE'
|
||||||
results = self.lib.items(q)
|
results = self.lib.items(q)
|
||||||
|
|
@ -182,6 +216,14 @@ class GetTest(DummyDataTestCase):
|
||||||
results = self.lib.items(q)
|
results = self.lib.items(q)
|
||||||
self.assert_items_matched(results, ['beets 4 eva'])
|
self.assert_items_matched(results, ['beets 4 eva'])
|
||||||
|
|
||||||
|
def test_keyed_matches_exact_nocase(self):
|
||||||
|
q = 'genre:~rock'
|
||||||
|
results = self.lib.items(q)
|
||||||
|
self.assert_items_matched(results, [
|
||||||
|
'foo bar',
|
||||||
|
'baz qux',
|
||||||
|
])
|
||||||
|
|
||||||
def test_unkeyed_term_matches_multiple_columns(self):
|
def test_unkeyed_term_matches_multiple_columns(self):
|
||||||
q = 'baz'
|
q = 'baz'
|
||||||
results = self.lib.items(q)
|
results = self.lib.items(q)
|
||||||
|
|
@ -350,6 +392,16 @@ class MatchTest(_common.TestCase):
|
||||||
q = dbcore.query.SubstringQuery('disc', '6')
|
q = dbcore.query.SubstringQuery('disc', '6')
|
||||||
self.assertTrue(q.match(self.item))
|
self.assertTrue(q.match(self.item))
|
||||||
|
|
||||||
|
def test_exact_match_nocase_positive(self):
|
||||||
|
q = dbcore.query.StringQuery('genre', 'the genre')
|
||||||
|
self.assertTrue(q.match(self.item))
|
||||||
|
q = dbcore.query.StringQuery('genre', 'THE GENRE')
|
||||||
|
self.assertTrue(q.match(self.item))
|
||||||
|
|
||||||
|
def test_exact_match_nocase_negative(self):
|
||||||
|
q = dbcore.query.StringQuery('genre', 'genre')
|
||||||
|
self.assertFalse(q.match(self.item))
|
||||||
|
|
||||||
def test_year_match_positive(self):
|
def test_year_match_positive(self):
|
||||||
q = dbcore.query.NumericQuery('year', '1')
|
q = dbcore.query.NumericQuery('year', '1')
|
||||||
self.assertTrue(q.match(self.item))
|
self.assertTrue(q.match(self.item))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue