mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 16:42:42 +01:00
added query string parser; library.get
--HG-- extra : convert_revision : svn%3A41726ec3-264d-0410-9c23-a9f1637257cc/trunk%4011
This commit is contained in:
parent
932f839bca
commit
5daba34d10
2 changed files with 96 additions and 9 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import sqlite3, os, sys, operator
|
import sqlite3, os, sys, operator, re
|
||||||
from beets.tag import MediaFile, FileTypeError
|
from beets.tag import MediaFile, FileTypeError
|
||||||
from string import Template
|
from string import Template
|
||||||
|
|
||||||
|
|
@ -187,6 +187,7 @@ class Item(object):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class QueryElement(object):
|
class QueryElement(object):
|
||||||
"""A building block for library queries."""
|
"""A building block for library queries."""
|
||||||
def clause(self):
|
def clause(self):
|
||||||
|
|
@ -198,19 +199,36 @@ class QueryElement(object):
|
||||||
class SubstringQueryElement(QueryElement):
|
class SubstringQueryElement(QueryElement):
|
||||||
"""A query element that matches a substring in a specific item field."""
|
"""A query element that matches a substring in a specific item field."""
|
||||||
|
|
||||||
def __init__(self, field, value):
|
def __init__(self, field, pattern):
|
||||||
if field not in item_keys:
|
if field not in item_keys:
|
||||||
raise InvalidFieldError(field + ' is not an item key')
|
raise InvalidFieldError(field + ' is not an item key')
|
||||||
self.field = field
|
self.field = field
|
||||||
self.value = value
|
self.pattern = pattern
|
||||||
|
|
||||||
def clause(self):
|
def clause(self):
|
||||||
search = '%' + (self.value.replace('\\','\\\\').replace('%','\\%')
|
search = '%' + (self.pattern.replace('\\','\\\\').replace('%','\\%')
|
||||||
.replace('_','\\_')) + '%'
|
.replace('_','\\_')) + '%'
|
||||||
clause = self.field + " like ? escape '\\'"
|
clause = self.field + " like ? escape '\\'"
|
||||||
subvals = [search]
|
subvals = [search]
|
||||||
return (clause, subvals)
|
return (clause, subvals)
|
||||||
|
|
||||||
|
class AnySubstringQueryElement(QueryElement):
|
||||||
|
"""A query element that matches a substring in any item field."""
|
||||||
|
|
||||||
|
def __init__(self, pattern):
|
||||||
|
self.pattern = pattern
|
||||||
|
|
||||||
|
def clause(self):
|
||||||
|
clause_parts = []
|
||||||
|
subvals = []
|
||||||
|
for field in item_keys:
|
||||||
|
el_clause, el_subvals = (SubstringQueryElement(field, self.pattern)
|
||||||
|
.clause())
|
||||||
|
clause_parts.append('(' + el_clause + ')')
|
||||||
|
subvals += el_subvals
|
||||||
|
clause = ' or '.join(clause_parts)
|
||||||
|
return clause, subvals
|
||||||
|
|
||||||
class AndQueryElement(QueryElement):
|
class AndQueryElement(QueryElement):
|
||||||
"""A conjunction of a list of other query elements. Can be indexed like a
|
"""A conjunction of a list of other query elements. Can be indexed like a
|
||||||
list to access the sub-elements."""
|
list to access the sub-elements."""
|
||||||
|
|
@ -237,7 +255,7 @@ class AndQueryElement(QueryElement):
|
||||||
clause_parts.append('(' + el_clause + ')')
|
clause_parts.append('(' + el_clause + ')')
|
||||||
subvals += el_subvals
|
subvals += el_subvals
|
||||||
clause = ' and '.join(clause_parts)
|
clause = ' and '.join(clause_parts)
|
||||||
return (clause, subvals)
|
return clause, subvals
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, matches):
|
def from_dict(cls, matches):
|
||||||
|
|
@ -248,6 +266,56 @@ class AndQueryElement(QueryElement):
|
||||||
elements.append(SubstringQueryElement(key, pattern))
|
elements.append(SubstringQueryElement(key, pattern))
|
||||||
return cls(elements)
|
return cls(elements)
|
||||||
|
|
||||||
|
|
||||||
|
# regular expression for _parse_query, below
|
||||||
|
_pq_regex = re.compile(r'(?:^|(?<=\s))' # zero-width match for whitespace or
|
||||||
|
# beginning of string
|
||||||
|
|
||||||
|
# non-grouping optional segment for the keyword
|
||||||
|
r'(?:'
|
||||||
|
r'(\S+?)' # the keyword
|
||||||
|
r'(?<!\\):' # unescaped :
|
||||||
|
r')?'
|
||||||
|
|
||||||
|
r'(\S+)', # the term itself, greedily consumed
|
||||||
|
re.I) # case-insensitive
|
||||||
|
@classmethod
|
||||||
|
def _parse_query(cls, query_string):
|
||||||
|
"""Takes a query in the form of a whitespace-separated list of search
|
||||||
|
terms that may be preceded with a key followed by a colon. Returns a
|
||||||
|
list of pairs (key, term) where key is None if the search term has no
|
||||||
|
key.
|
||||||
|
|
||||||
|
For instance,
|
||||||
|
parse_query('stapler color:red') ==
|
||||||
|
[(None, 'stapler'), ('color', 'red')]
|
||||||
|
|
||||||
|
Colons may be 'escaped' with a backslash to disable the keying
|
||||||
|
behavior.
|
||||||
|
"""
|
||||||
|
out = []
|
||||||
|
for match in cls._pq_regex.finditer(query_string):
|
||||||
|
out.append((match.group(1), match.group(2).replace(r'\:',':')))
|
||||||
|
return out
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_string(cls, query_string):
|
||||||
|
"""Creates a query from a string in the format used by _parse_query."""
|
||||||
|
elements = []
|
||||||
|
for key, pattern in cls._parse_query(query_string):
|
||||||
|
if key is None: # no key specified; match any field
|
||||||
|
elements.append(AnySubstringQueryElement(pattern))
|
||||||
|
elif key.lower() in item_keys: # ignore unrecognized keys
|
||||||
|
elements.append(SubstringQueryElement(key.lower(), pattern))
|
||||||
|
if not elements: # no terms in query
|
||||||
|
elements = [TrueQueryElement()]
|
||||||
|
return cls(elements)
|
||||||
|
|
||||||
|
class TrueQueryElement(QueryElement):
|
||||||
|
"""A query element that always matches."""
|
||||||
|
def clause(self):
|
||||||
|
return '1', ()
|
||||||
|
|
||||||
class Query(AndQueryElement):
|
class Query(AndQueryElement):
|
||||||
"""A query into the item database."""
|
"""A query into the item database."""
|
||||||
|
|
||||||
|
|
@ -263,10 +331,10 @@ class Query(AndQueryElement):
|
||||||
ItemResultIterator."""
|
ItemResultIterator."""
|
||||||
cursor = library.conn.cursor()
|
cursor = library.conn.cursor()
|
||||||
cursor.execute(*self.statement())
|
cursor.execute(*self.statement())
|
||||||
return ItemResultIterator(cursor)
|
return ResultIterator(cursor)
|
||||||
|
|
||||||
class ItemResultIterator(object):
|
class ResultIterator(object):
|
||||||
"""An iterator into an item result set."""
|
"""An iterator into an item query result set."""
|
||||||
|
|
||||||
def __init__(self, cursor):
|
def __init__(self, cursor):
|
||||||
self.cursor = cursor
|
self.cursor = cursor
|
||||||
|
|
@ -287,6 +355,7 @@ class ItemResultIterator(object):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Library(object):
|
class Library(object):
|
||||||
def __init__(self, path='library.blb'):
|
def __init__(self, path='library.blb'):
|
||||||
self.path = path
|
self.path = path
|
||||||
|
|
@ -355,6 +424,17 @@ class Library(object):
|
||||||
else: # something else: special file?
|
else: # something else: special file?
|
||||||
self.__log(path + ' special file, skipping')
|
self.__log(path + ' special file, skipping')
|
||||||
|
|
||||||
|
def get(self, query):
|
||||||
|
"""Returns a ResultIterator to the items matching query, which may be
|
||||||
|
None (match the entire library), a Query object, or a query string."""
|
||||||
|
if query is None:
|
||||||
|
query = Query([TrueQueryElement()])
|
||||||
|
elif isinstance(query, str) or isinstance(query, unicode):
|
||||||
|
query = Query.from_string(query)
|
||||||
|
elif not isinstance(query, Query):
|
||||||
|
raise ValueError('query must be None or have type Query or str')
|
||||||
|
return query.execute(self)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""Writes the library to disk (completing a sqlite transaction)."""
|
"""Writes the library to disk (completing a sqlite transaction)."""
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
|
||||||
9
bts.py
9
bts.py
|
|
@ -7,6 +7,13 @@ def add(lib, paths):
|
||||||
lib.add(path)
|
lib.add(path)
|
||||||
lib.save()
|
lib.save()
|
||||||
|
|
||||||
|
def ls(lib, criteria):
|
||||||
|
q = ' '.join(criteria)
|
||||||
|
if not q.strip():
|
||||||
|
q = None # no criteria => match anything
|
||||||
|
for item in lib.get(q):
|
||||||
|
print item.artist + ' - ' + item.album + ' - ' + item.title
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# parse options
|
# parse options
|
||||||
usage = """usage: %prog [options] command
|
usage = """usage: %prog [options] command
|
||||||
|
|
@ -34,7 +41,7 @@ command is one of: add, remove, update, write, list, help"""
|
||||||
#(remove, ['remove', 'rm']),
|
#(remove, ['remove', 'rm']),
|
||||||
#(update, ['update', 'up']),
|
#(update, ['update', 'up']),
|
||||||
#(write, ['write', 'wr', 'w']),
|
#(write, ['write', 'wr', 'w']),
|
||||||
#(list, ['list', 'ls']),
|
(ls, ['list', 'ls']),
|
||||||
(help, ['help', 'h'])
|
(help, ['help', 'h'])
|
||||||
]
|
]
|
||||||
for test_command in avail_commands:
|
for test_command in avail_commands:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue