mirror of
https://github.com/beetbox/beets.git
synced 2025-12-07 09:04:33 +01:00
some low-level tweaks to extensible queries (#214)
This commit is contained in:
parent
292092bef7
commit
40b49ac786
6 changed files with 61 additions and 78 deletions
|
|
@ -17,7 +17,6 @@
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import difflib
|
|
||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
import shlex
|
import shlex
|
||||||
|
|
@ -515,7 +514,6 @@ class RegexpQuery(FieldQuery):
|
||||||
value = util.as_string(getattr(item, self.field))
|
value = util.as_string(getattr(item, self.field))
|
||||||
return self.regexp.search(value) is not None
|
return self.regexp.search(value) is not None
|
||||||
|
|
||||||
|
|
||||||
class PluginQuery(FieldQuery):
|
class PluginQuery(FieldQuery):
|
||||||
"""The base class to add queries using beets plugins. Plugins can add
|
"""The base class to add queries using beets plugins. Plugins can add
|
||||||
special queries by defining a subclass of PluginQuery and overriding
|
special queries by defining a subclass of PluginQuery and overriding
|
||||||
|
|
@ -525,9 +523,18 @@ class PluginQuery(FieldQuery):
|
||||||
super(PluginQuery, self).__init__(field, pattern)
|
super(PluginQuery, self).__init__(field, pattern)
|
||||||
|
|
||||||
def clause(self):
|
def clause(self):
|
||||||
clause = "{name}(?, {field})".format(name=self.__class__.__name__, field=self.field)
|
# Invoke the registered SQLite function.
|
||||||
|
clause = "{name}(?, {field})".format(name=self.__class__.__name__,
|
||||||
|
field=self.field)
|
||||||
return clause, [self.pattern]
|
return clause, [self.pattern]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register(cls, conn):
|
||||||
|
"""Register this query's matching function with the SQLite
|
||||||
|
connection.
|
||||||
|
"""
|
||||||
|
conn.create_function(cls.__name__, 2, cls(None, None).match)
|
||||||
|
|
||||||
class BooleanQuery(MatchQuery):
|
class BooleanQuery(MatchQuery):
|
||||||
"""Matches a boolean field. Pattern should either be a boolean or a
|
"""Matches a boolean field. Pattern should either be a boolean or a
|
||||||
string reflecting a boolean.
|
string reflecting a boolean.
|
||||||
|
|
@ -593,11 +600,14 @@ class CollectionQuery(Query):
|
||||||
@classmethod
|
@classmethod
|
||||||
def _parse_query_part(cls, part):
|
def _parse_query_part(cls, part):
|
||||||
"""Takes a query in the form of a key/value pair separated by a
|
"""Takes a query in the form of a key/value pair separated by a
|
||||||
colon. The value part is matched against a list of prefixes that can be
|
colon. The value part is matched against a list of prefixes that
|
||||||
extended by plugins to add custom query types. For example, the colon
|
can be extended by plugins to add custom query types. For
|
||||||
prefix denotes a regular exporession query.
|
example, the colon prefix denotes a regular expression query.
|
||||||
|
|
||||||
The function returns a tuple of(key, value, Query)
|
The function returns a tuple of `(key, value, cls)`. `key` may
|
||||||
|
be None, indicating that any field may be matched. `cls` is
|
||||||
|
either a subclass of `PluginQuery` or `None` indicating a
|
||||||
|
"normal" query.
|
||||||
|
|
||||||
For instance,
|
For instance,
|
||||||
parse_query('stapler') == (None, 'stapler', None)
|
parse_query('stapler') == (None, 'stapler', None)
|
||||||
|
|
@ -611,17 +621,17 @@ class CollectionQuery(Query):
|
||||||
part = part.strip()
|
part = part.strip()
|
||||||
match = cls._pq_regex.match(part)
|
match = cls._pq_regex.match(part)
|
||||||
|
|
||||||
cls.prefixes = {':': RegexpQuery}
|
prefixes = {':': RegexpQuery}
|
||||||
cls.prefixes.update(plugins.queries())
|
prefixes.update(plugins.queries())
|
||||||
|
|
||||||
if match:
|
if match:
|
||||||
key = match.group(1)
|
key = match.group(1)
|
||||||
term = match.group(2).replace('\:', ':')
|
term = match.group(2).replace('\:', ':')
|
||||||
# match the search term against the list of prefixes
|
# Match the search term against the list of prefixes.
|
||||||
for pre, query in cls.prefixes.items():
|
for pre, query_class in prefixes.items():
|
||||||
if term.startswith(pre):
|
if term.startswith(pre):
|
||||||
return (key, term[len(pre):], query)
|
return key, term[len(pre):], query_class
|
||||||
return (key, term, None) # None means a normal query
|
return key, term, None # None means a normal query.
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_strings(cls, query_parts, default_fields=None,
|
def from_strings(cls, query_parts, default_fields=None,
|
||||||
|
|
@ -637,7 +647,7 @@ class CollectionQuery(Query):
|
||||||
if not res:
|
if not res:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
key, pattern, prefix_query = res
|
key, pattern, query_class = res
|
||||||
|
|
||||||
# No key specified.
|
# No key specified.
|
||||||
if key is None:
|
if key is None:
|
||||||
|
|
@ -646,8 +656,9 @@ class CollectionQuery(Query):
|
||||||
subqueries.append(PathQuery(pattern))
|
subqueries.append(PathQuery(pattern))
|
||||||
else:
|
else:
|
||||||
# Match any field.
|
# Match any field.
|
||||||
if prefix_query:
|
if query_class:
|
||||||
subq = AnyPluginQuery(pattern, default_fields, cls=prefix_query)
|
subq = AnyPluginQuery(pattern, default_fields,
|
||||||
|
cls=query_class)
|
||||||
else:
|
else:
|
||||||
subq = AnySubstringQuery(pattern, default_fields)
|
subq = AnySubstringQuery(pattern, default_fields)
|
||||||
subqueries.append(subq)
|
subqueries.append(subq)
|
||||||
|
|
@ -662,8 +673,8 @@ class CollectionQuery(Query):
|
||||||
|
|
||||||
# Other (recognized) field.
|
# Other (recognized) field.
|
||||||
elif key.lower() in all_keys:
|
elif key.lower() in all_keys:
|
||||||
if prefix_query is not None:
|
if query_class:
|
||||||
subqueries.append(prefix_query(key.lower(), pattern))
|
subqueries.append(query_class(key.lower(), pattern))
|
||||||
else:
|
else:
|
||||||
subqueries.append(SubstringQuery(key.lower(), pattern))
|
subqueries.append(SubstringQuery(key.lower(), pattern))
|
||||||
|
|
||||||
|
|
@ -724,42 +735,10 @@ class AnySubstringQuery(CollectionQuery):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
class AnyRegexpQuery(CollectionQuery):
|
|
||||||
"""A query that matches a regexp in any of a list of metadata
|
|
||||||
fields.
|
|
||||||
"""
|
|
||||||
def __init__(self, pattern, fields=None):
|
|
||||||
"""Create a query for regexp over the sequence of fields
|
|
||||||
given. If no fields are given, all available fields are
|
|
||||||
used.
|
|
||||||
"""
|
|
||||||
self.regexp = re.compile(pattern)
|
|
||||||
self.fields = fields or ITEM_KEYS_WRITABLE
|
|
||||||
|
|
||||||
subqueries = []
|
|
||||||
for field in self.fields:
|
|
||||||
subqueries.append(RegexpQuery(field, pattern))
|
|
||||||
super(AnyRegexpQuery, self).__init__(subqueries)
|
|
||||||
|
|
||||||
def clause(self):
|
|
||||||
return self.clause_with_joiner('or')
|
|
||||||
|
|
||||||
def match(self, item):
|
|
||||||
for fld in self.fields:
|
|
||||||
try:
|
|
||||||
val = getattr(item, fld)
|
|
||||||
except KeyError:
|
|
||||||
continue
|
|
||||||
if isinstance(val, basestring) and \
|
|
||||||
self.regexp.match(val) is not None:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
class AnyPluginQuery(CollectionQuery):
|
class AnyPluginQuery(CollectionQuery):
|
||||||
"""A query that dispatch the matching function to the match method of
|
"""A query that dispatch the matching function to the match method of
|
||||||
the cls provided to the contstructor using a list of metadata fields.
|
the cls provided to the contstructor using a list of metadata fields.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, pattern, fields=None, cls=PluginQuery):
|
def __init__(self, pattern, fields=None, cls=PluginQuery):
|
||||||
subqueries = []
|
subqueries = []
|
||||||
self.pattern = pattern
|
self.pattern = pattern
|
||||||
|
|
@ -1174,9 +1153,9 @@ class Library(BaseLibrary):
|
||||||
# Add the REGEXP function to SQLite queries.
|
# Add the REGEXP function to SQLite queries.
|
||||||
conn.create_function("REGEXP", 2, _regexp)
|
conn.create_function("REGEXP", 2, _regexp)
|
||||||
|
|
||||||
# Register plugin queries
|
# Register plugin queries.
|
||||||
for prefix, query in plugins.queries().items():
|
for prefix, query_class in plugins.queries().items():
|
||||||
conn.create_function(query.__name__, 2, query(None, None).match)
|
query_class.register(conn)
|
||||||
|
|
||||||
self._connections[thread_id] = conn
|
self._connections[thread_id] = conn
|
||||||
return conn
|
return conn
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,9 @@ class BeetsPlugin(object):
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
def queries(self):
|
def queries(self):
|
||||||
"""Should return a dict of {prefix : beets.library.PluginQuery}"""
|
"""Should return a dict mapping prefixes to PluginQuery
|
||||||
|
subclasses.
|
||||||
|
"""
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def track_distance(self, item, info):
|
def track_distance(self, item, info):
|
||||||
|
|
@ -215,8 +217,9 @@ def commands():
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def queries():
|
def queries():
|
||||||
"""Returns a dict of {prefix: beet.library.PluginQuery} objects from all
|
"""Returns a dict mapping prefix strings to beet.library.PluginQuery
|
||||||
loaded plugins. """
|
subclasses all loaded plugins.
|
||||||
|
"""
|
||||||
out = {}
|
out = {}
|
||||||
for plugin in find_plugins():
|
for plugin in find_plugins():
|
||||||
out.update(plugin.queries())
|
out.update(plugin.queries())
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,11 @@
|
||||||
# The above copyright notice and this permission notice shall be
|
# The above copyright notice and this permission notice shall be
|
||||||
# included in all copies or substantial portions of the Software.
|
# included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
"""Like beet list, but with fuzzy matching
|
"""Provides a fuzzy matching query.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from beets.plugins import BeetsPlugin
|
from beets.plugins import BeetsPlugin
|
||||||
from beets.library import PluginQuery
|
from beets.library import PluginQuery
|
||||||
from beets.ui import Subcommand, decargs, print_obj
|
|
||||||
from beets import util
|
from beets import util
|
||||||
import beets
|
import beets
|
||||||
from beets.util import confit
|
from beets.util import confit
|
||||||
|
|
@ -28,7 +27,7 @@ class FuzzyQuery(PluginQuery):
|
||||||
def __init__(self, field, pattern):
|
def __init__(self, field, pattern):
|
||||||
super(FuzzyQuery, self).__init__(field, pattern)
|
super(FuzzyQuery, self).__init__(field, pattern)
|
||||||
try:
|
try:
|
||||||
self.threshold = beets.config['fuzzy']['threshold'].as_number()
|
self.threshold = beets.config['fuzzy']['threshold'].as_number()
|
||||||
except confit.NotFoundError:
|
except confit.NotFoundError:
|
||||||
self.threshold = 0.7
|
self.threshold = 0.7
|
||||||
|
|
||||||
|
|
@ -37,7 +36,7 @@ class FuzzyQuery(PluginQuery):
|
||||||
return False
|
return False
|
||||||
val = util.as_string(val)
|
val = util.as_string(val)
|
||||||
# smartcase
|
# smartcase
|
||||||
if(pattern.islower()):
|
if pattern.islower():
|
||||||
val = val.lower()
|
val = val.lower()
|
||||||
queryMatcher = difflib.SequenceMatcher(None, pattern, val)
|
queryMatcher = difflib.SequenceMatcher(None, pattern, val)
|
||||||
return queryMatcher.quick_ratio() >= self.threshold
|
return queryMatcher.quick_ratio() >= self.threshold
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,12 @@ Other stuff:
|
||||||
track in MusicBrainz and updates your library to reflect it. This can help
|
track in MusicBrainz and updates your library to reflect it. This can help
|
||||||
you easily correct errors that have been fixed in the MB database. Thanks to
|
you easily correct errors that have been fixed in the MB database. Thanks to
|
||||||
Jakob Schnitzer.
|
Jakob Schnitzer.
|
||||||
|
* :doc:`/plugins/fuzzy`: The ``fuzzy`` command was removed and replaced with a
|
||||||
|
new query type. To perform fuzzy searches, use the ``~`` prefix with
|
||||||
|
:ref:`list-cmd` or other commands. Thanks to Philippe Mongeau.
|
||||||
|
* As part of the above, plugins can now extend the query syntax and new kinds
|
||||||
|
of matching capabilities to beets. See :ref:`extend-query`. Thanks again to
|
||||||
|
Philippe Mongeau.
|
||||||
* :doc:`/plugins/convert`: A new ``--keep-new`` option lets you store
|
* :doc:`/plugins/convert`: A new ``--keep-new`` option lets you store
|
||||||
transcoded files in your library while backing up the originals (instead of
|
transcoded files in your library while backing up the originals (instead of
|
||||||
vice-versa). Thanks to Lucas Duailibe.
|
vice-versa). Thanks to Lucas Duailibe.
|
||||||
|
|
@ -59,8 +65,6 @@ Other stuff:
|
||||||
the Echo Nest library.
|
the Echo Nest library.
|
||||||
* :doc:`/plugins/chroma`: Catch Acoustid Web service errors when submitting
|
* :doc:`/plugins/chroma`: Catch Acoustid Web service errors when submitting
|
||||||
fingerprints.
|
fingerprints.
|
||||||
* :ref:`extend-query`: Plugins can now extend the query syntax. Thanks to
|
|
||||||
Philippe Mongeau
|
|
||||||
|
|
||||||
1.1b2 (February 16, 2013)
|
1.1b2 (February 16, 2013)
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
Fuzzy Search Plugin
|
Fuzzy Search Plugin
|
||||||
===================
|
===================
|
||||||
|
|
||||||
The ``fuzzy`` plugin provides a query prefix that search you library using fuzzy
|
The ``fuzzy`` plugin provides a prefixed query that search you library using
|
||||||
pattern matching. This can be useful if you want to find a track with
|
fuzzy pattern matching. This can be useful if you want to find a track with
|
||||||
complicated characters in the title.
|
complicated characters in the title.
|
||||||
|
|
||||||
First, enable the plugin named ``fuzzy`` (see :doc:`/plugins/index`).
|
First, enable the plugin named ``fuzzy`` (see :doc:`/plugins/index`).
|
||||||
|
|
@ -11,14 +11,14 @@ You'll then be able to use the ``~`` prefix to use fuzzy matching::
|
||||||
$ beet ls '~Vareoldur'
|
$ beet ls '~Vareoldur'
|
||||||
Sigur Rós - Valtari - Varðeldur
|
Sigur Rós - Valtari - Varðeldur
|
||||||
|
|
||||||
The plugin provides to config option to let you choose the prefix and the
|
The plugin provides config options that let you choose the prefix and the
|
||||||
threshold.::
|
threshold.::
|
||||||
|
|
||||||
fuzzy:
|
fuzzy:
|
||||||
threshold: 0.8
|
threshold: 0.8
|
||||||
prefix: '@'
|
prefix: '@'
|
||||||
|
|
||||||
A threshold value of ``1`` will show only perfect matches and a value of ``0``
|
A threshold value of 1.0 will show only perfect matches and a value of 0.0
|
||||||
will match everything.
|
will match everything.
|
||||||
|
|
||||||
The default prefix ``~`` needs to be escaped or quoted in most shells. If this
|
The default prefix ``~`` needs to be escaped or quoted in most shells. If this
|
||||||
|
|
|
||||||
|
|
@ -327,32 +327,30 @@ to register it::
|
||||||
.. _extend-query:
|
.. _extend-query:
|
||||||
|
|
||||||
Extend the Query Syntax
|
Extend the Query Syntax
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Beets already support searching using regular expressions by prepending search
|
You can add new kinds of queries to beets' :doc:`query syntax
|
||||||
terms with the colon prefix. It is possible to add new prefix by extending the
|
</reference/query>` indicated by a prefix. As an example, beets already
|
||||||
``PluginQuery`` class.
|
supports regular expression queries, which are indicated by a colon
|
||||||
|
prefix---plugins can do the same.
|
||||||
|
|
||||||
The plugin then need to declare its new queries by returning a ``dict`` of
|
To do so, define a subclass of the ``PluginQuery`` type from the
|
||||||
``{prefix: PluginQuery}`` from the ``queries`` method.
|
``beets.library`` module. Then, in the ``queries`` method of your plugin
|
||||||
|
class, return a dictionary mapping prefix strings to query classes.
|
||||||
|
|
||||||
The following example plugins declares a query using the ``@`` prefix. So the
|
The following example plugins declares a query using the ``@`` prefix. So the
|
||||||
plugin will be called if we issue a command like ``beet ls @something`` or
|
plugin will be called if we issue a command like ``beet ls @something`` or
|
||||||
``beet ls artist:@something``::
|
``beet ls artist:@something``::
|
||||||
|
|
||||||
from beets.plugins import BeetsPlugin
|
from beets.plugins import BeetsPlugin
|
||||||
from beets.Library import PluginQuery
|
from beets.library import PluginQuery
|
||||||
|
|
||||||
class ExampleQuery(PluginQuery):
|
class ExampleQuery(PluginQuery):
|
||||||
def match(self, pattern, val):
|
def match(self, pattern, val):
|
||||||
return True # this will simply match everything
|
return True # This will just match everything.
|
||||||
|
|
||||||
class ExamplePlugin(BeetsPlugin):
|
class ExamplePlugin(BeetsPlugin):
|
||||||
def queries():
|
def queries():
|
||||||
# plugins need to declare theire queries by
|
|
||||||
# returning a dict of {prefix: PluginQuery}
|
|
||||||
# from the queries() function
|
|
||||||
return {
|
return {
|
||||||
'@': ExampleQuery
|
'@': ExampleQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue