Anonymous avatar Anonymous committed 869d49a

added query string parser; library.get

Comments (0)

Files changed (2)

-import sqlite3, os, sys, operator
+import sqlite3, os, sys, operator, re
 from beets.tag import MediaFile, FileTypeError
 from string import Template
 
 
 
 
+
 class QueryElement(object):
     """A building block for library queries."""
     def clause(self):
 class SubstringQueryElement(QueryElement):
     """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:
             raise InvalidFieldError(field + ' is not an item key')
         self.field = field
-        self.value = value
+        self.pattern = pattern
     
     def clause(self):
-        search = '%' + (self.value.replace('\\','\\\\').replace('%','\\%')
+        search = '%' + (self.pattern.replace('\\','\\\\').replace('%','\\%')
                             .replace('_','\\_')) + '%'
         clause = self.field + " like ? escape '\\'"
         subvals = [search]
         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):
     """A conjunction of a list of other query elements. Can be indexed like a
     list to access the sub-elements."""
             clause_parts.append('(' + el_clause + ')')
             subvals += el_subvals
         clause = ' and '.join(clause_parts)
-        return (clause, subvals)
+        return clause, subvals
     
     @classmethod
     def from_dict(cls, matches):
             elements.append(SubstringQueryElement(key, pattern))
         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):
     """A query into the item database."""
     
         ItemResultIterator."""
         cursor = library.conn.cursor()
         cursor.execute(*self.statement())
-        return ItemResultIterator(cursor)
+        return ResultIterator(cursor)
 
-class ItemResultIterator(object):
-    """An iterator into an item result set."""
+class ResultIterator(object):
+    """An iterator into an item query result set."""
     
     def __init__(self, cursor):
         self.cursor = cursor
 
 
 
+
 class Library(object):
     def __init__(self, path='library.blb'):
         self.path = path
         else: # something else: special file?
             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):
         """Writes the library to disk (completing a sqlite transaction)."""
         self.conn.commit()
         lib.add(path)
     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__":
     # parse options
     usage = """usage: %prog [options] command
         #(remove,     ['remove', 'rm']),
         #(update,     ['update', 'up']),
         #(write,      ['write', 'wr', 'w']),
-        #(list,       ['list', 'ls']),
+        (ls,         ['list', 'ls']),
         (help,       ['help', 'h'])
     ]
     for test_command in avail_commands:
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.