mirror of
https://github.com/beetbox/beets.git
synced 2025-12-13 12:02:44 +01:00
Merge branch 'master' of https://github.com/sampsyo/beets
This commit is contained in:
commit
bd3f1ca5c0
18 changed files with 858 additions and 83 deletions
|
|
@ -12,7 +12,7 @@
|
|||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
__version__ = '1.3.7'
|
||||
__version__ = '1.3.8'
|
||||
__author__ = 'Adrian Sampson <adrian@radbox.org>'
|
||||
|
||||
import beets.library
|
||||
|
|
|
|||
|
|
@ -55,6 +55,9 @@ list_format_item: $artist - $album - $title
|
|||
list_format_album: $albumartist - $album
|
||||
time_format: '%Y-%m-%d %H:%M:%S'
|
||||
|
||||
sort_album: smartartist+
|
||||
sort_item: smartartist+
|
||||
|
||||
paths:
|
||||
default: $albumartist/$album%aunique{}/$track $title
|
||||
singleton: Non-Album/$artist/$title
|
||||
|
|
|
|||
|
|
@ -19,5 +19,6 @@ from .db import Model, Database
|
|||
from .query import Query, FieldQuery, MatchQuery, AndQuery, OrQuery
|
||||
from .types import Type
|
||||
from .queryparse import query_from_strings
|
||||
from .queryparse import sort_from_strings
|
||||
|
||||
# flake8: noqa
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import collections
|
|||
|
||||
import beets
|
||||
from beets.util.functemplate import Template
|
||||
from .query import MatchQuery
|
||||
from .query import MatchQuery, build_sql
|
||||
from .types import BASE_TYPE
|
||||
|
||||
|
||||
|
|
@ -483,40 +483,64 @@ class Results(object):
|
|||
"""An item query result set. Iterating over the collection lazily
|
||||
constructs LibModel objects that reflect database rows.
|
||||
"""
|
||||
def __init__(self, model_class, rows, db, query=None):
|
||||
def __init__(self, model_class, rows, db, query=None, sort=None):
|
||||
"""Create a result set that will construct objects of type
|
||||
`model_class`, which should be a subclass of `LibModel`, out of
|
||||
the query result mapping in `rows`. The new objects are
|
||||
associated with the database `db`. If `query` is provided, it is
|
||||
used as a predicate to filter the results for a "slow query" that
|
||||
cannot be evaluated by the database directly.
|
||||
associated with the database `db`.
|
||||
If `query` is provided, it is used as a predicate to filter the results
|
||||
for a "slow query" that cannot be evaluated by the database directly.
|
||||
If `sort` is provided, it is used to sort the full list of results
|
||||
before returning. This means it is a "slow sort" and all objects must
|
||||
be built before returning the first one.
|
||||
"""
|
||||
self.model_class = model_class
|
||||
self.rows = rows
|
||||
self.db = db
|
||||
self.query = query
|
||||
self.sort = sort
|
||||
|
||||
def __iter__(self):
|
||||
"""Construct Python objects for all rows that pass the query
|
||||
predicate.
|
||||
"""
|
||||
for row in self.rows:
|
||||
# Get the flexible attributes for the object.
|
||||
with self.db.transaction() as tx:
|
||||
flex_rows = tx.query(
|
||||
'SELECT * FROM {0} WHERE entity_id=?'.format(
|
||||
self.model_class._flex_table
|
||||
),
|
||||
(row['id'],)
|
||||
)
|
||||
values = dict(row)
|
||||
flex_values = dict((row['key'], row['value']) for row in flex_rows)
|
||||
if self.sort:
|
||||
# Slow sort. Must build the full list first.
|
||||
objects = []
|
||||
for row in self.rows:
|
||||
obj = self._make_model(row)
|
||||
# check the predicate if any
|
||||
if not self.query or self.query.match(obj):
|
||||
objects.append(obj)
|
||||
# Now that we have the full list, we can sort it
|
||||
objects = self.sort.sort(objects)
|
||||
for o in objects:
|
||||
yield o
|
||||
else:
|
||||
for row in self.rows:
|
||||
obj = self._make_model(row)
|
||||
# check the predicate if any
|
||||
if not self.query or self.query.match(obj):
|
||||
yield obj
|
||||
|
||||
# Construct the Python object and yield it if it passes the
|
||||
# predicate.
|
||||
obj = self.model_class._awaken(self.db, values, flex_values)
|
||||
if not self.query or self.query.match(obj):
|
||||
yield obj
|
||||
def _make_model(self, row):
|
||||
# Get the flexible attributes for the object.
|
||||
with self.db.transaction() as tx:
|
||||
flex_rows = tx.query(
|
||||
'SELECT * FROM {0} WHERE entity_id=?'.format(
|
||||
self.model_class._flex_table
|
||||
),
|
||||
(row['id'],)
|
||||
)
|
||||
|
||||
cols = dict(row)
|
||||
values = dict((k, v) for (k, v) in cols.items()
|
||||
if not k[:4] == 'flex')
|
||||
flex_values = dict((row['key'], row['value']) for row in flex_rows)
|
||||
|
||||
# Construct the Python object
|
||||
obj = self.model_class._awaken(self.db, values, flex_values)
|
||||
return obj
|
||||
|
||||
def __len__(self):
|
||||
"""Get the number of matching objects.
|
||||
|
|
@ -739,24 +763,20 @@ class Database(object):
|
|||
|
||||
# Querying.
|
||||
|
||||
def _fetch(self, model_cls, query, order_by=None):
|
||||
def _fetch(self, model_cls, query, sort_order=None):
|
||||
"""Fetch the objects of type `model_cls` matching the given
|
||||
query. The query may be given as a string, string sequence, a
|
||||
Query object, or None (to fetch everything). If provided,
|
||||
`order_by` is a SQLite ORDER BY clause for sorting.
|
||||
"""
|
||||
where, subvals = query.clause()
|
||||
`sort_order` is either a SQLite ORDER BY clause for sorting or a
|
||||
Sort object.
|
||||
"""
|
||||
|
||||
sql, subvals, query, sort = build_sql(model_cls, query, sort_order)
|
||||
|
||||
sql = "SELECT * FROM {0} WHERE {1}".format(
|
||||
model_cls._table,
|
||||
where or '1',
|
||||
)
|
||||
if order_by:
|
||||
sql += " ORDER BY {0}".format(order_by)
|
||||
with self.transaction() as tx:
|
||||
rows = tx.query(sql, subvals)
|
||||
|
||||
return Results(model_cls, rows, self, None if where else query)
|
||||
return Results(model_cls, rows, self, query, sort)
|
||||
|
||||
def _get(self, model_cls, id):
|
||||
"""Get a Model object by its id or None if the id does not
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"""The Query type hierarchy for DBCore.
|
||||
"""
|
||||
import re
|
||||
from operator import attrgetter
|
||||
from beets import util
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
|
@ -497,3 +498,257 @@ class DateQuery(FieldQuery):
|
|||
# Match any date.
|
||||
clause = '1'
|
||||
return clause, subvals
|
||||
|
||||
|
||||
class Sort(object):
|
||||
"""An abstract class representing a sort operation for a query into the
|
||||
item database.
|
||||
"""
|
||||
def select_clause(self):
|
||||
""" Generates a select sql fragment if the sort operation requires one,
|
||||
an empty string otherwise.
|
||||
"""
|
||||
return ""
|
||||
|
||||
def union_clause(self):
|
||||
""" Generates a union sql fragment if the sort operation requires one,
|
||||
an empty string otherwise.
|
||||
"""
|
||||
return ""
|
||||
|
||||
def order_clause(self):
|
||||
"""Generates a sql fragment to be use in a ORDER BY clause or None if
|
||||
it's a slow query.
|
||||
"""
|
||||
return None
|
||||
|
||||
def sort(self, items):
|
||||
"""Return a key function that can be used with the list.sort() method.
|
||||
Meant to be used with slow sort, it must be implemented even for sort
|
||||
that can be done with sql, as they might be used in conjunction with
|
||||
slow sort.
|
||||
"""
|
||||
return sorted(items, key=lambda x: x)
|
||||
|
||||
def is_slow(self):
|
||||
return False
|
||||
|
||||
|
||||
class MultipleSort(Sort):
|
||||
"""Sort class that combines several sort criteria.
|
||||
This implementation tries to implement as many sort operation in sql,
|
||||
falling back to python sort only when necessary.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.sorts = []
|
||||
|
||||
def add_criteria(self, sort):
|
||||
self.sorts.append(sort)
|
||||
|
||||
def _sql_sorts(self):
|
||||
""" Returns the list of sort for which sql can be used
|
||||
"""
|
||||
# with several Sort, we can use SQL sorting only if there is only
|
||||
# SQL-capable Sort or if the list ends with SQl-capable Sort.
|
||||
sql_sorts = []
|
||||
for sort in reversed(self.sorts):
|
||||
if not sort.order_clause() is None:
|
||||
sql_sorts.append(sort)
|
||||
else:
|
||||
break
|
||||
sql_sorts.reverse()
|
||||
return sql_sorts
|
||||
|
||||
def select_clause(self):
|
||||
sql_sorts = self._sql_sorts()
|
||||
select_strings = []
|
||||
for sort in sql_sorts:
|
||||
select = sort.select_clause()
|
||||
if select:
|
||||
select_strings.append(select)
|
||||
|
||||
select_string = ",".join(select_strings)
|
||||
return select_string
|
||||
|
||||
def union_clause(self):
|
||||
sql_sorts = self._sql_sorts()
|
||||
union_strings = []
|
||||
for sort in sql_sorts:
|
||||
union = sort.union_clause()
|
||||
union_strings.append(union)
|
||||
|
||||
return "".join(union_strings)
|
||||
|
||||
def order_clause(self):
|
||||
sql_sorts = self._sql_sorts()
|
||||
order_strings = []
|
||||
for sort in sql_sorts:
|
||||
order = sort.order_clause()
|
||||
order_strings.append(order)
|
||||
|
||||
return ",".join(order_strings)
|
||||
|
||||
def is_slow(self):
|
||||
for sort in self.sorts:
|
||||
if sort.is_slow():
|
||||
return True
|
||||
return False
|
||||
|
||||
def sort(self, items):
|
||||
slow_sorts = []
|
||||
switch_slow = False
|
||||
for sort in reversed(self.sorts):
|
||||
if switch_slow:
|
||||
slow_sorts.append(sort)
|
||||
elif sort.order_clause() is None:
|
||||
switch_slow = True
|
||||
slow_sorts.append(sort)
|
||||
else:
|
||||
pass
|
||||
|
||||
for sort in slow_sorts:
|
||||
items = sort.sort(items)
|
||||
return items
|
||||
|
||||
|
||||
class FlexFieldSort(Sort):
|
||||
"""Sort object to sort on a flexible attribute field
|
||||
"""
|
||||
def __init__(self, model_cls, field, is_ascending):
|
||||
self.model_cls = model_cls
|
||||
self.field = field
|
||||
self.is_ascending = is_ascending
|
||||
|
||||
def select_clause(self):
|
||||
""" Return a select sql fragment.
|
||||
"""
|
||||
return "sort_flexattr{0!s}.value as flex_{0!s} ".format(self.field)
|
||||
|
||||
def union_clause(self):
|
||||
""" Returns an union sql fragment.
|
||||
"""
|
||||
union = ("LEFT JOIN {flextable} as sort_flexattr{index!s} "
|
||||
"ON {table}.id = sort_flexattr{index!s}.entity_id "
|
||||
"AND sort_flexattr{index!s}.key='{flexattr}' ").format(
|
||||
flextable=self.model_cls._flex_table,
|
||||
table=self.model_cls._table,
|
||||
index=self.field, flexattr=self.field)
|
||||
return union
|
||||
|
||||
def order_clause(self):
|
||||
""" Returns an order sql fragment.
|
||||
"""
|
||||
order = "ASC" if self.is_ascending else "DESC"
|
||||
return "flex_{0} {1} ".format(self.field, order)
|
||||
|
||||
def sort(self, items):
|
||||
return sorted(items, key=attrgetter(self.field),
|
||||
reverse=(not self.is_ascending))
|
||||
|
||||
|
||||
class FixedFieldSort(Sort):
|
||||
"""Sort object to sort on a fixed field
|
||||
"""
|
||||
def __init__(self, field, is_ascending=True):
|
||||
self.field = field
|
||||
self.is_ascending = is_ascending
|
||||
|
||||
def order_clause(self):
|
||||
order = "ASC" if self.is_ascending else "DESC"
|
||||
return "{0} {1}".format(self.field, order)
|
||||
|
||||
def sort(self, items):
|
||||
return sorted(items, key=attrgetter(self.field),
|
||||
reverse=(not self.is_ascending))
|
||||
|
||||
|
||||
class SmartArtistSort(Sort):
|
||||
""" Sort Album or Item on artist sort fields, defaulting back on
|
||||
artist field if the sort specific field is empty.
|
||||
"""
|
||||
def __init__(self, model_cls, is_ascending=True):
|
||||
self.model_cls = model_cls
|
||||
self.is_ascending = is_ascending
|
||||
|
||||
def select_clause(self):
|
||||
return ""
|
||||
|
||||
def union_clause(self):
|
||||
return ""
|
||||
|
||||
def order_clause(self):
|
||||
order = "ASC" if self.is_ascending else "DESC"
|
||||
if 'albumartist_sort' in self.model_cls._fields:
|
||||
exp1 = 'albumartist_sort'
|
||||
exp2 = 'albumartist'
|
||||
elif 'artist_sort' in self.model_cls_fields:
|
||||
exp1 = 'artist_sort'
|
||||
exp2 = 'artist'
|
||||
else:
|
||||
return ""
|
||||
|
||||
order_str = ('(CASE {0} WHEN NULL THEN {1} '
|
||||
'WHEN "" THEN {1} '
|
||||
'ELSE {0} END) {2} ').format(exp1, exp2, order)
|
||||
return order_str
|
||||
|
||||
|
||||
class ComputedFieldSort(Sort):
|
||||
|
||||
def __init__(self, model_cls, field, is_ascending=True):
|
||||
self.is_ascending = is_ascending
|
||||
self.field = field
|
||||
self._getters = model_cls._getters()
|
||||
|
||||
def is_slow(self):
|
||||
return True
|
||||
|
||||
def sort(self, items):
|
||||
return sorted(items, key=lambda x: self._getters[self.field](x),
|
||||
reverse=(not self.is_ascending))
|
||||
|
||||
special_sorts = {'smartartist': SmartArtistSort}
|
||||
|
||||
|
||||
def build_sql(model_cls, query, sort):
|
||||
""" Generate a sql statement (and the values that must be injected into it)
|
||||
from a query, sort and a model class. Query and sort objects are returned
|
||||
only for slow query and slow sort operation.
|
||||
"""
|
||||
where, subvals = query.clause()
|
||||
if where is not None:
|
||||
query = None
|
||||
|
||||
if not sort:
|
||||
sort_select = ""
|
||||
sort_union = ""
|
||||
sort_order = ""
|
||||
sort = None
|
||||
elif isinstance(sort, basestring):
|
||||
sort_select = ""
|
||||
sort_union = ""
|
||||
sort_order = " ORDER BY {0}".format(sort) \
|
||||
if sort else ""
|
||||
sort = None
|
||||
elif isinstance(sort, Sort):
|
||||
select_clause = sort.select_clause()
|
||||
sort_select = " ,{0} ".format(select_clause) \
|
||||
if select_clause else ""
|
||||
sort_union = sort.union_clause()
|
||||
order_clause = sort.order_clause()
|
||||
sort_order = " ORDER BY {0}".format(order_clause) \
|
||||
if order_clause else ""
|
||||
if sort.is_slow():
|
||||
sort = None
|
||||
|
||||
sql = ("SELECT {table}.* {sort_select} FROM {table} {sort_union} WHERE "
|
||||
"{query_clause} {sort_order}").format(
|
||||
sort_select=sort_select,
|
||||
sort_union=sort_union,
|
||||
table=model_cls._table,
|
||||
query_clause=where or '1',
|
||||
sort_order=sort_order
|
||||
)
|
||||
|
||||
return sql, subvals, query, sort
|
||||
|
|
|
|||
|
|
@ -121,3 +121,33 @@ def query_from_strings(query_cls, model_cls, prefixes, query_parts):
|
|||
if not subqueries: # No terms in query.
|
||||
subqueries = [query.TrueQuery()]
|
||||
return query_cls(subqueries)
|
||||
|
||||
|
||||
def construct_sort_part(model_cls, part):
|
||||
""" Creates a Sort object from a single criteria. Returns a `Sort` instance.
|
||||
"""
|
||||
sort = None
|
||||
field = part[:-1]
|
||||
is_ascending = (part[-1] == '+')
|
||||
if field in model_cls._fields:
|
||||
sort = query.FixedFieldSort(field, is_ascending)
|
||||
elif field in model_cls._getters():
|
||||
# Computed field, all following fields must use the slow path.
|
||||
sort = query.ComputedFieldSort(model_cls, field, is_ascending)
|
||||
elif field in query.special_sorts:
|
||||
sort = query.special_sorts[field](model_cls, is_ascending)
|
||||
else:
|
||||
# Neither fixed nor computed : must be a flex attr.
|
||||
sort = query.FlexFieldSort(model_cls, field, is_ascending)
|
||||
return sort
|
||||
|
||||
|
||||
def sort_from_strings(model_cls, sort_parts):
|
||||
"""Creates a Sort object from a list of sort criteria strings.
|
||||
"""
|
||||
if not sort_parts:
|
||||
return None
|
||||
sort = query.MultipleSort()
|
||||
for part in sort_parts:
|
||||
sort.add_criteria(construct_sort_part(model_cls, part))
|
||||
return sort
|
||||
|
|
|
|||
|
|
@ -548,7 +548,7 @@ class Item(LibModel):
|
|||
for query, path_format in path_formats:
|
||||
if query == PF_KEY_DEFAULT:
|
||||
continue
|
||||
query = get_query(query, type(self))
|
||||
(query, _) = get_query_sort(query, type(self))
|
||||
if query.match(self):
|
||||
# The query matches the item! Use the corresponding path
|
||||
# format.
|
||||
|
|
@ -896,9 +896,10 @@ class Album(LibModel):
|
|||
|
||||
# Query construction helper.
|
||||
|
||||
def get_query(val, model_cls):
|
||||
def get_query_sort(val, model_cls):
|
||||
"""Take a value which may be None, a query string, a query string
|
||||
list, or a Query object, and return a suitable Query object.
|
||||
list, or a Query object, and return a suitable Query object and Sort
|
||||
object.
|
||||
|
||||
`model_cls` is the subclass of Model indicating which entity this
|
||||
is a query for (i.e., Album or Item) and is used to determine which
|
||||
|
|
@ -919,7 +920,7 @@ def get_query(val, model_cls):
|
|||
val = [s.decode('utf8') for s in shlex.split(val)]
|
||||
|
||||
if val is None:
|
||||
return dbcore.query.TrueQuery()
|
||||
return (dbcore.query.TrueQuery(), None)
|
||||
|
||||
elif isinstance(val, list) or isinstance(val, tuple):
|
||||
# Special-case path-like queries, which are non-field queries
|
||||
|
|
@ -937,18 +938,23 @@ def get_query(val, model_cls):
|
|||
path_parts = ()
|
||||
non_path_parts = val
|
||||
|
||||
# separate query token and sort token
|
||||
query_val = [s for s in non_path_parts if not s.endswith(('+', '-'))]
|
||||
sort_val = [s for s in non_path_parts if s.endswith(('+', '-'))]
|
||||
|
||||
# Parse remaining parts and construct an AndQuery.
|
||||
query = dbcore.query_from_strings(
|
||||
dbcore.AndQuery, model_cls, prefixes, non_path_parts
|
||||
dbcore.AndQuery, model_cls, prefixes, query_val
|
||||
)
|
||||
sort = dbcore.sort_from_strings(model_cls, sort_val)
|
||||
|
||||
# Add path queries to aggregate query.
|
||||
if path_parts:
|
||||
query.subqueries += [PathQuery('path', s) for s in path_parts]
|
||||
return query
|
||||
return query, sort
|
||||
|
||||
elif isinstance(val, dbcore.Query):
|
||||
return val
|
||||
return val, None
|
||||
|
||||
else:
|
||||
raise ValueError('query must be None or have type Query or str')
|
||||
|
|
@ -1015,30 +1021,30 @@ class Library(dbcore.Database):
|
|||
|
||||
# Querying.
|
||||
|
||||
def _fetch(self, model_cls, query, order_by=None):
|
||||
"""Parse a query and fetch.
|
||||
"""
|
||||
def _fetch(self, model_cls, query, sort_order=None):
|
||||
"""Parse a query and fetch. If a order specification is present in the
|
||||
query string the sort_order argument is ignored.
|
||||
"""
|
||||
query, sort = get_query_sort(query, model_cls)
|
||||
sort = sort or sort_order
|
||||
|
||||
return super(Library, self)._fetch(
|
||||
model_cls, get_query(query, model_cls), order_by
|
||||
model_cls, query, sort
|
||||
)
|
||||
|
||||
def albums(self, query=None):
|
||||
def albums(self, query=None, sort_order=None):
|
||||
"""Get a sorted list of :class:`Album` objects matching the
|
||||
given query.
|
||||
given sort order. If a order specification is present in the query
|
||||
string the sort_order argument is ignored.
|
||||
"""
|
||||
order = '{0}, album'.format(
|
||||
_orelse("albumartist_sort", "albumartist")
|
||||
)
|
||||
return self._fetch(Album, query, order)
|
||||
return self._fetch(Album, query, sort_order)
|
||||
|
||||
def items(self, query=None):
|
||||
def items(self, query=None, sort_order=None):
|
||||
"""Get a sorted list of :class:`Item` objects matching the given
|
||||
query.
|
||||
given sort order. If a order specification is present in the query
|
||||
string the sort_order argument is ignored.
|
||||
"""
|
||||
order = '{0}, album, disc, track'.format(
|
||||
_orelse("artist_sort", "artist")
|
||||
)
|
||||
return self._fetch(Item, query, order)
|
||||
return self._fetch(Item, query, sort_order)
|
||||
|
||||
# Convenience accessors.
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ from beets.util.functemplate import Template
|
|||
from beets import library
|
||||
from beets import config
|
||||
from beets.util.confit import _package_path
|
||||
from beets.dbcore import sort_from_strings
|
||||
|
||||
VARIOUS_ARTISTS = u'Various Artists'
|
||||
|
||||
|
|
@ -966,11 +967,18 @@ def list_items(lib, query, album, fmt):
|
|||
albums instead of single items.
|
||||
"""
|
||||
tmpl = Template(ui._pick_format(album, fmt))
|
||||
|
||||
if album:
|
||||
for album in lib.albums(query):
|
||||
sort_parts = str(config['sort_album']).split()
|
||||
sort_order = sort_from_strings(library.Album,
|
||||
sort_parts)
|
||||
for album in lib.albums(query, sort_order):
|
||||
ui.print_obj(album, lib, tmpl)
|
||||
else:
|
||||
for item in lib.items(query):
|
||||
sort_parts = str(config['sort_item']).split()
|
||||
sort_order = sort_from_strings(library.Item,
|
||||
sort_parts)
|
||||
for item in lib.items(query, sort_order):
|
||||
ui.print_obj(item, lib, tmpl)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
import logging
|
||||
from beets.plugins import BeetsPlugin
|
||||
from beets.importer import action
|
||||
from beets.library import get_query
|
||||
from beets.library import get_query_sort
|
||||
from beets.library import Item
|
||||
from beets.library import Album
|
||||
|
||||
|
|
@ -57,9 +57,9 @@ class IHatePlugin(BeetsPlugin):
|
|||
for query_string in action_patterns:
|
||||
query = None
|
||||
if task.is_album:
|
||||
query = get_query(query_string, Album)
|
||||
(query, _) = get_query_sort(query_string, Album)
|
||||
else:
|
||||
query = get_query(query_string, Item)
|
||||
(query, _) = get_query_sort(query_string, Item)
|
||||
if any(query.match(item) for item in task.imported_items()):
|
||||
return True
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ def _items_for_query(lib, playlist, album=False):
|
|||
query_strings = [query_strings]
|
||||
model = library.Album if album else library.Item
|
||||
query = dbcore.OrQuery(
|
||||
[library.get_query(q, model) for q in query_strings]
|
||||
[library.get_query_sort(q, model)[0] for q in query_strings]
|
||||
)
|
||||
|
||||
# Execute query, depending on type.
|
||||
|
|
|
|||
|
|
@ -1,35 +1,52 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
1.3.7 (in development)
|
||||
1.3.8 (in development)
|
||||
----------------------
|
||||
|
||||
New stuff
|
||||
This release adds **sorting** to beets queries. See :ref:`query-sort`.
|
||||
|
||||
|
||||
1.3.7 (August 22, 2014)
|
||||
-----------------------
|
||||
|
||||
This release of beets fixes all the bugs, and you can be confident that you
|
||||
will never again find any bugs in beets, ever.
|
||||
It also adds support for plain old AIFF files and adds three more plugins,
|
||||
including a nifty one that lets you measure a song's tempo by tapping out the
|
||||
beat on your keyboard.
|
||||
The importer deals more elegantly with duplicates and you can broaden your
|
||||
cover art search to the entire web with Google Image Search.
|
||||
|
||||
The big new features are:
|
||||
|
||||
* Support for AIFF files. Tags are stored as ID3 frames in one of the file's
|
||||
IFF chunks. Thanks to Evan Purkhiser for contributing support to `Mutagen`_.
|
||||
* The new :doc:`/plugins/importadded` reads files' modification times to set
|
||||
their "added" date. Thanks to Stig Inge Lea Bjørnsen.
|
||||
* Support for AIFF files. Tags are stored as ID3 frames in one of the file's
|
||||
IFF chunks.
|
||||
* A new :ref:`required` configuration option for the importer skips matches
|
||||
that are missing certain data. Thanks to oprietop.
|
||||
* The new :doc:`/plugins/bpm` lets you manually measure the tempo of a playing
|
||||
song. Thanks to aroquen.
|
||||
* The new :doc:`/plugins/spotify` generates playlists for your `Spotify`_
|
||||
account. Thanks to Olin Gay.
|
||||
* A new :ref:`required` configuration option for the importer skips matches
|
||||
that are missing certain data. Thanks to oprietop.
|
||||
* When the importer detects duplicates, it now shows you some details about
|
||||
the potentially-replaced music so you can make an informed decision. Thanks
|
||||
to Howard Jones.
|
||||
* :doc:`/plugins/fetchart`: You can now optionally search for cover art on
|
||||
Google Image Search. Thanks to Lemutar.
|
||||
* A new :ref:`asciify-paths` configuration option replaces all non-ASCII
|
||||
characters in paths.
|
||||
|
||||
.. _Mutagen: https://bitbucket.org/lazka/mutagen
|
||||
.. _Spotify: https://www.spotify.com/
|
||||
|
||||
Little improvements and fixes:
|
||||
And the multitude of little improvements and fixes:
|
||||
|
||||
* Compatibility with the latest version of `Mutagen`_, 1.23.
|
||||
* :doc:`/plugins/web`: Lyrics now display readably with correct line breaks.
|
||||
Also, the detail view scrolls to reveal all of the lyrics. Thanks to Meet
|
||||
Udeshi.
|
||||
* Compatibility with the latest version of Mutagen, 1.23.
|
||||
* :doc:`/plugins/fetchart`: You can now optionally search for cover art on
|
||||
Google Image Search. Thanks to Lemutar.
|
||||
* :doc:`/plugins/play`: The ``command`` config option can now contain
|
||||
arguments (rather than just an executable). Thanks to Alessandro Ghedini.
|
||||
* Fix an error when using the :ref:`modify-cmd` command to remove a flexible
|
||||
|
|
@ -51,11 +68,11 @@ Little improvements and fixes:
|
|||
* Don't display changes for fields that are not in the restricted field set.
|
||||
This fixes :ref:`write-cmd` showing changes for fields that are not written
|
||||
to the file.
|
||||
* :ref:`write-cmd` command: Don't display the item name if there are no
|
||||
changes for it.
|
||||
* When using both :doc:`/plugins/convert` and :doc:`/plugins/scrub`, avoid
|
||||
scrubbing the source file of conversions. (Fix a regression introduced in
|
||||
the previous release.)
|
||||
* The :ref:`write-cmd` command avoids displaying the item name if there are
|
||||
no changes for it.
|
||||
* When using both the :doc:`/plugins/convert` and the :doc:`/plugins/scrub`,
|
||||
avoid scrubbing the source file of conversions. (Fix a regression introduced
|
||||
in the previous release.)
|
||||
* :doc:`/plugins/replaygain`: Logging is now quieter during import. Thanks to
|
||||
Yevgeny Bezman.
|
||||
* :doc:`/plugins/fetchart`: When loading art from the filesystem, we now
|
||||
|
|
@ -71,7 +88,7 @@ Little improvements and fixes:
|
|||
* :doc:`/plugins/bucket`: You can now customize the definition of alphanumeric
|
||||
"ranges" using regular expressions. And the heuristic for detecting years
|
||||
has been improved. Thanks to sotho.
|
||||
* Already imported singleton tracks are skipped when resuming an
|
||||
* Already-imported singleton tracks are skipped when resuming an
|
||||
import.
|
||||
* :doc:`/plugins/chroma`: A new ``auto`` configuration option disables
|
||||
fingerprinting on import. Thanks to ddettrittus.
|
||||
|
|
@ -79,8 +96,6 @@ Little improvements and fixes:
|
|||
transcoding preset from the command-line.
|
||||
* :doc:`/plugins/convert`: Transcoding presets can now omit their filename
|
||||
extensions (extensions default to the name of the preset).
|
||||
* A new :ref:`asciify-paths` configuration option replaces all non-ASCII
|
||||
characters in paths.
|
||||
* :doc:`/plugins/convert`: A new ``--pretend`` option lets you preview the
|
||||
commands the plugin will execute without actually taking any action. Thanks
|
||||
to Dietrich Daroch.
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ project = u'beets'
|
|||
copyright = u'2012, Adrian Sampson'
|
||||
|
||||
version = '1.3'
|
||||
release = '1.3.7'
|
||||
release = '1.3.8'
|
||||
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
|
|
|
|||
|
|
@ -174,8 +174,9 @@ list
|
|||
|
||||
Want to search for "Gronlandic Edit" by of Montreal? Try ``beet list
|
||||
gronlandic``. Maybe you want to see everything released in 2009 with
|
||||
"vegetables" in the title? Try ``beet list year:2009 title:vegetables``. (Read
|
||||
more in :doc:`query`.)
|
||||
"vegetables" in the title? Try ``beet list year:2009 title:vegetables``. You
|
||||
can also specify the order used when outputting the results (Read more in
|
||||
:doc:`query`.)
|
||||
|
||||
You can use the ``-a`` switch to search for albums instead of individual items.
|
||||
In this case, the queries you use are restricted to album-level fields: for
|
||||
|
|
|
|||
|
|
@ -188,6 +188,24 @@ Format to use when listing *albums* with :ref:`list-cmd` and other
|
|||
commands. Defaults to ``$albumartist - $album``. The ``-f`` command-line
|
||||
option overrides this setting.
|
||||
|
||||
.. _sort_item:
|
||||
|
||||
sort_item
|
||||
~~~~~~~~~
|
||||
|
||||
Sort order to use when listing *individual items* with the :ref:`list-cmd`
|
||||
command and other commands that need to print out items. Defaults to
|
||||
``smartartist+``. Any command-line sort order overrides this setting.
|
||||
|
||||
.. _sort_album:
|
||||
|
||||
sort_album
|
||||
~~~~~~~~~~
|
||||
|
||||
Sort order to use when listing *albums* with the :ref:`list-cmd`
|
||||
command. Defaults to ``smartartist+``. Any command-line sort order overrides
|
||||
this setting.
|
||||
|
||||
.. _original_date:
|
||||
|
||||
original_date
|
||||
|
|
|
|||
|
|
@ -183,3 +183,32 @@ equivalent::
|
|||
Note that this only matches items that are *already in your library*, so a path
|
||||
query won't necessarily find *all* the audio files in a directory---just the
|
||||
ones you've already added to your beets library.
|
||||
|
||||
|
||||
.. _query-sort:
|
||||
|
||||
Sort Order
|
||||
----------
|
||||
|
||||
You can also specify the order used when outputting the results. Of course, this
|
||||
is only useful when displaying the result, for example with the ``list``
|
||||
command, and is useless when the query is used as a filter for an command. Use
|
||||
the name of the `field` you want to sort on, followed by a ``+`` or ``-`` sign
|
||||
if you want ascending or descending sort. For example this command::
|
||||
|
||||
$ beet list -a year+
|
||||
|
||||
will list all albums in chronological order.
|
||||
|
||||
There is a special ``smartartist`` sort that uses sort-specific field (
|
||||
``artist_sort`` for items and ``albumartist_sort`` for albums) but falls back to
|
||||
standard artist fields if these are empty. When no sort order is specified,
|
||||
``smartartist+`` is used (but this is configurable).
|
||||
|
||||
You can also specify several sort orders, which will be used in the same order at
|
||||
which they appear in your query::
|
||||
|
||||
$ beet list -a genre+ year+
|
||||
|
||||
This command will sort all albums by genre and, in each genre, in chronological
|
||||
order.
|
||||
|
|
|
|||
|
|
@ -85,6 +85,30 @@ def item(lib=None):
|
|||
lib.add(i)
|
||||
return i
|
||||
|
||||
_album_ident = 0
|
||||
def album(lib=None):
|
||||
global _item_ident
|
||||
_item_ident += 1
|
||||
i = beets.library.Album(
|
||||
artpath= None,
|
||||
albumartist = 'some album artist',
|
||||
albumartist_sort = 'some sort album artist',
|
||||
albumartist_credit = 'some album artist credit',
|
||||
album = 'the album',
|
||||
genre = 'the genre',
|
||||
year = 2014,
|
||||
month = 2,
|
||||
day = 5,
|
||||
tracktotal = 0,
|
||||
disctotal = 1,
|
||||
comp = False,
|
||||
mb_albumid = 'someID-1',
|
||||
mb_albumartistid = 'someID-1'
|
||||
)
|
||||
if lib:
|
||||
lib.add(i)
|
||||
return i
|
||||
|
||||
# Dummy import session.
|
||||
def import_session(lib=None, logfile=None, paths=[], query=[], cli=False):
|
||||
cls = commands.TerminalImportSession if cli else importer.ImportSession
|
||||
|
|
|
|||
|
|
@ -412,6 +412,37 @@ class QueryFromStringsTest(_common.TestCase):
|
|||
self.assertIsInstance(q.subqueries[0], dbcore.query.NumericQuery)
|
||||
|
||||
|
||||
class SortFromStringsTest(_common.TestCase):
|
||||
def sfs(self, strings):
|
||||
return dbcore.queryparse.sort_from_strings(
|
||||
TestModel1,
|
||||
strings,
|
||||
)
|
||||
|
||||
def test_zero_parts(self):
|
||||
s = self.sfs([])
|
||||
self.assertIsNone(s)
|
||||
|
||||
def test_one_parts(self):
|
||||
s = self.sfs(['field+'])
|
||||
self.assertIsInstance(s, dbcore.query.Sort)
|
||||
|
||||
def test_two_parts(self):
|
||||
s = self.sfs(['field+', 'another_field-'])
|
||||
self.assertIsInstance(s, dbcore.query.MultipleSort)
|
||||
self.assertEqual(len(s.sorts), 2)
|
||||
|
||||
def test_fixed_field_sort(self):
|
||||
s = self.sfs(['field_one+'])
|
||||
self.assertIsInstance(s, dbcore.query.MultipleSort)
|
||||
self.assertIsInstance(s.sorts[0], dbcore.query.FixedFieldSort)
|
||||
|
||||
def test_flex_field_sort(self):
|
||||
s = self.sfs(['flex_field+'])
|
||||
self.assertIsInstance(s, dbcore.query.MultipleSort)
|
||||
self.assertIsInstance(s.sorts[0], dbcore.query.FlexFieldSort)
|
||||
|
||||
|
||||
def suite():
|
||||
return unittest.TestLoader().loadTestsFromName(__name__)
|
||||
|
||||
|
|
|
|||
334
test/test_sort.py
Normal file
334
test/test_sort.py
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
# This file is part of beets.
|
||||
# Copyright 2013, Adrian Sampson.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
|
||||
"""Various tests for querying the library database.
|
||||
"""
|
||||
import _common
|
||||
from _common import unittest
|
||||
import beets.library
|
||||
from beets import dbcore
|
||||
|
||||
|
||||
# A test case class providing a library with some dummy data and some
|
||||
# assertions involving that data.
|
||||
class DummyDataTestCase(_common.TestCase):
|
||||
def setUp(self):
|
||||
super(DummyDataTestCase, self).setUp()
|
||||
self.lib = beets.library.Library(':memory:')
|
||||
|
||||
albums = [_common.album() for _ in range(3)]
|
||||
albums[0].album = "album A"
|
||||
albums[0].genre = "Rock"
|
||||
albums[0].year = "2001"
|
||||
albums[0].flex1 = "flex1-1"
|
||||
albums[0].flex2 = "flex2-A"
|
||||
albums[1].album = "album B"
|
||||
albums[1].genre = "Rock"
|
||||
albums[1].year = "2001"
|
||||
albums[1].flex1 = "flex1-2"
|
||||
albums[1].flex2 = "flex2-A"
|
||||
albums[2].album = "album C"
|
||||
albums[2].genre = "Jazz"
|
||||
albums[2].year = "2005"
|
||||
albums[2].flex1 = "flex1-1"
|
||||
albums[2].flex2 = "flex2-B"
|
||||
for album in albums:
|
||||
self.lib.add(album)
|
||||
|
||||
items = [_common.item() for _ in range(4)]
|
||||
items[0].title = 'foo bar'
|
||||
items[0].artist = 'one'
|
||||
items[0].album = 'baz'
|
||||
items[0].year = 2001
|
||||
items[0].comp = True
|
||||
items[0].flex1 = "flex1-0"
|
||||
items[0].flex2 = "flex2-A"
|
||||
items[0].album_id = albums[0].id
|
||||
items[1].title = 'baz qux'
|
||||
items[1].artist = 'two'
|
||||
items[1].album = 'baz'
|
||||
items[1].year = 2002
|
||||
items[1].comp = True
|
||||
items[1].flex1 = "flex1-1"
|
||||
items[1].flex2 = "flex2-A"
|
||||
items[1].album_id = albums[0].id
|
||||
items[2].title = 'beets 4 eva'
|
||||
items[2].artist = 'three'
|
||||
items[2].album = 'foo'
|
||||
items[2].year = 2003
|
||||
items[2].comp = False
|
||||
items[2].flex1 = "flex1-2"
|
||||
items[2].flex2 = "flex1-B"
|
||||
items[2].album_id = albums[1].id
|
||||
items[3].title = 'beets 4 eva'
|
||||
items[3].artist = 'three'
|
||||
items[3].album = 'foo2'
|
||||
items[3].year = 2004
|
||||
items[3].comp = False
|
||||
items[3].flex1 = "flex1-2"
|
||||
items[3].flex2 = "flex1-C"
|
||||
items[3].album_id = albums[2].id
|
||||
for item in items:
|
||||
self.lib.add(item)
|
||||
|
||||
|
||||
class SortFixedFieldTest(DummyDataTestCase):
|
||||
def test_sort_asc(self):
|
||||
q = ''
|
||||
sort = dbcore.query.FixedFieldSort("year", True)
|
||||
results = self.lib.items(q, sort)
|
||||
self.assertLessEqual(results[0]['year'], results[1]['year'])
|
||||
self.assertEqual(results[0]['year'], 2001)
|
||||
# same thing with query string
|
||||
q = 'year+'
|
||||
results2 = self.lib.items(q)
|
||||
for r1, r2 in zip(results, results2):
|
||||
self.assertEqual(r1.id, r2.id)
|
||||
|
||||
def test_sort_desc(self):
|
||||
q = ''
|
||||
sort = dbcore.query.FixedFieldSort("year", False)
|
||||
results = self.lib.items(q, sort)
|
||||
self.assertGreaterEqual(results[0]['year'], results[1]['year'])
|
||||
self.assertEqual(results[0]['year'], 2004)
|
||||
# same thing with query string
|
||||
q = 'year-'
|
||||
results2 = self.lib.items(q)
|
||||
for r1, r2 in zip(results, results2):
|
||||
self.assertEqual(r1.id, r2.id)
|
||||
|
||||
def test_sort_two_field_asc(self):
|
||||
q = ''
|
||||
s1 = dbcore.query.FixedFieldSort("album", True)
|
||||
s2 = dbcore.query.FixedFieldSort("year", True)
|
||||
sort = dbcore.query.MultipleSort()
|
||||
sort.add_criteria(s1)
|
||||
sort.add_criteria(s2)
|
||||
results = self.lib.items(q, sort)
|
||||
self.assertLessEqual(results[0]['album'], results[1]['album'])
|
||||
self.assertLessEqual(results[1]['album'], results[2]['album'])
|
||||
self.assertEqual(results[0]['album'], 'baz')
|
||||
self.assertEqual(results[1]['album'], 'baz')
|
||||
self.assertLessEqual(results[0]['year'], results[1]['year'])
|
||||
# same thing with query string
|
||||
q = 'album+ year+'
|
||||
results2 = self.lib.items(q)
|
||||
for r1, r2 in zip(results, results2):
|
||||
self.assertEqual(r1.id, r2.id)
|
||||
|
||||
|
||||
class SortFlexFieldTest(DummyDataTestCase):
|
||||
def test_sort_asc(self):
|
||||
q = ''
|
||||
sort = dbcore.query.FlexFieldSort(beets.library.Item, "flex1", True)
|
||||
results = self.lib.items(q, sort)
|
||||
self.assertLessEqual(results[0]['flex1'], results[1]['flex1'])
|
||||
self.assertEqual(results[0]['flex1'], 'flex1-0')
|
||||
# same thing with query string
|
||||
q = 'flex1+'
|
||||
results2 = self.lib.items(q)
|
||||
for r1, r2 in zip(results, results2):
|
||||
self.assertEqual(r1.id, r2.id)
|
||||
|
||||
def test_sort_desc(self):
|
||||
q = ''
|
||||
sort = dbcore.query.FlexFieldSort(beets.library.Item, "flex1", False)
|
||||
results = self.lib.items(q, sort)
|
||||
self.assertGreaterEqual(results[0]['flex1'], results[1]['flex1'])
|
||||
self.assertGreaterEqual(results[1]['flex1'], results[2]['flex1'])
|
||||
self.assertGreaterEqual(results[2]['flex1'], results[3]['flex1'])
|
||||
self.assertEqual(results[0]['flex1'], 'flex1-2')
|
||||
# same thing with query string
|
||||
q = 'flex1-'
|
||||
results2 = self.lib.items(q)
|
||||
for r1, r2 in zip(results, results2):
|
||||
self.assertEqual(r1.id, r2.id)
|
||||
|
||||
def test_sort_two_field(self):
|
||||
q = ''
|
||||
s1 = dbcore.query.FlexFieldSort(beets.library.Item, "flex2", False)
|
||||
s2 = dbcore.query.FlexFieldSort(beets.library.Item, "flex1", True)
|
||||
sort = dbcore.query.MultipleSort()
|
||||
sort.add_criteria(s1)
|
||||
sort.add_criteria(s2)
|
||||
results = self.lib.items(q, sort)
|
||||
self.assertGreaterEqual(results[0]['flex2'], results[1]['flex2'])
|
||||
self.assertGreaterEqual(results[1]['flex2'], results[2]['flex2'])
|
||||
self.assertEqual(results[0]['flex2'], 'flex2-A')
|
||||
self.assertEqual(results[1]['flex2'], 'flex2-A')
|
||||
self.assertLessEqual(results[0]['flex1'], results[1]['flex1'])
|
||||
# same thing with query string
|
||||
q = 'flex2- flex1+'
|
||||
results2 = self.lib.items(q)
|
||||
for r1, r2 in zip(results, results2):
|
||||
self.assertEqual(r1.id, r2.id)
|
||||
|
||||
|
||||
class SortAlbumFixedFieldTest(DummyDataTestCase):
|
||||
def test_sort_asc(self):
|
||||
q = ''
|
||||
sort = dbcore.query.FixedFieldSort("year", True)
|
||||
results = self.lib.albums(q, sort)
|
||||
self.assertLessEqual(results[0]['year'], results[1]['year'])
|
||||
self.assertEqual(results[0]['year'], 2001)
|
||||
# same thing with query string
|
||||
q = 'year+'
|
||||
results2 = self.lib.albums(q)
|
||||
for r1, r2 in zip(results, results2):
|
||||
self.assertEqual(r1.id, r2.id)
|
||||
|
||||
def test_sort_desc(self):
|
||||
q = ''
|
||||
sort = dbcore.query.FixedFieldSort("year", False)
|
||||
results = self.lib.albums(q, sort)
|
||||
self.assertGreaterEqual(results[0]['year'], results[1]['year'])
|
||||
self.assertEqual(results[0]['year'], 2005)
|
||||
# same thing with query string
|
||||
q = 'year-'
|
||||
results2 = self.lib.albums(q)
|
||||
for r1, r2 in zip(results, results2):
|
||||
self.assertEqual(r1.id, r2.id)
|
||||
|
||||
def test_sort_two_field_asc(self):
|
||||
q = ''
|
||||
s1 = dbcore.query.FixedFieldSort("genre", True)
|
||||
s2 = dbcore.query.FixedFieldSort("album", True)
|
||||
sort = dbcore.query.MultipleSort()
|
||||
sort.add_criteria(s1)
|
||||
sort.add_criteria(s2)
|
||||
results = self.lib.albums(q, sort)
|
||||
self.assertLessEqual(results[0]['genre'], results[1]['genre'])
|
||||
self.assertLessEqual(results[1]['genre'], results[2]['genre'])
|
||||
self.assertEqual(results[1]['genre'], 'Rock')
|
||||
self.assertEqual(results[2]['genre'], 'Rock')
|
||||
self.assertLessEqual(results[1]['album'], results[2]['album'])
|
||||
# same thing with query string
|
||||
q = 'genre+ album+'
|
||||
results2 = self.lib.albums(q)
|
||||
for r1, r2 in zip(results, results2):
|
||||
self.assertEqual(r1.id, r2.id)
|
||||
|
||||
|
||||
class SortAlbumFlexdFieldTest(DummyDataTestCase):
|
||||
def test_sort_asc(self):
|
||||
q = ''
|
||||
sort = dbcore.query.FlexFieldSort(beets.library.Album, "flex1", True)
|
||||
results = self.lib.albums(q, sort)
|
||||
self.assertLessEqual(results[0]['flex1'], results[1]['flex1'])
|
||||
self.assertLessEqual(results[1]['flex1'], results[2]['flex1'])
|
||||
# same thing with query string
|
||||
q = 'flex1+'
|
||||
results2 = self.lib.albums(q)
|
||||
for r1, r2 in zip(results, results2):
|
||||
self.assertEqual(r1.id, r2.id)
|
||||
|
||||
def test_sort_desc(self):
|
||||
q = ''
|
||||
sort = dbcore.query.FlexFieldSort(beets.library.Album, "flex1", False)
|
||||
results = self.lib.albums(q, sort)
|
||||
self.assertGreaterEqual(results[0]['flex1'], results[1]['flex1'])
|
||||
self.assertGreaterEqual(results[1]['flex1'], results[2]['flex1'])
|
||||
# same thing with query string
|
||||
q = 'flex1-'
|
||||
results2 = self.lib.albums(q)
|
||||
for r1, r2 in zip(results, results2):
|
||||
self.assertEqual(r1.id, r2.id)
|
||||
|
||||
def test_sort_two_field_asc(self):
|
||||
q = ''
|
||||
s1 = dbcore.query.FlexFieldSort(beets.library.Album, "flex2", True)
|
||||
s2 = dbcore.query.FlexFieldSort(beets.library.Album, "flex1", True)
|
||||
sort = dbcore.query.MultipleSort()
|
||||
sort.add_criteria(s1)
|
||||
sort.add_criteria(s2)
|
||||
results = self.lib.albums(q, sort)
|
||||
self.assertLessEqual(results[0]['flex2'], results[1]['flex2'])
|
||||
self.assertLessEqual(results[1]['flex2'], results[2]['flex2'])
|
||||
self.assertEqual(results[0]['flex2'], 'flex2-A')
|
||||
self.assertEqual(results[1]['flex2'], 'flex2-A')
|
||||
self.assertLessEqual(results[0]['flex1'], results[1]['flex1'])
|
||||
# same thing with query string
|
||||
q = 'flex2+ flex1+'
|
||||
results2 = self.lib.albums(q)
|
||||
for r1, r2 in zip(results, results2):
|
||||
self.assertEqual(r1.id, r2.id)
|
||||
|
||||
|
||||
class SortAlbumComputedFieldTest(DummyDataTestCase):
|
||||
def test_sort_asc(self):
|
||||
q = ''
|
||||
sort = dbcore.query.ComputedFieldSort(beets.library.Album, "path",
|
||||
True)
|
||||
results = self.lib.albums(q, sort)
|
||||
self.assertLessEqual(results[0]['path'], results[1]['path'])
|
||||
self.assertLessEqual(results[1]['path'], results[2]['path'])
|
||||
# same thing with query string
|
||||
q = 'path+'
|
||||
results2 = self.lib.albums(q)
|
||||
for r1, r2 in zip(results, results2):
|
||||
self.assertEqual(r1.id, r2.id)
|
||||
|
||||
def test_sort_desc(self):
|
||||
q = ''
|
||||
sort = dbcore.query.ComputedFieldSort(beets.library.Album, "path",
|
||||
False)
|
||||
results = self.lib.albums(q, sort)
|
||||
self.assertGreaterEqual(results[0]['path'], results[1]['path'])
|
||||
self.assertGreaterEqual(results[1]['path'], results[2]['path'])
|
||||
# same thing with query string
|
||||
q = 'path-'
|
||||
results2 = self.lib.albums(q)
|
||||
for r1, r2 in zip(results, results2):
|
||||
self.assertEqual(r1.id, r2.id)
|
||||
|
||||
|
||||
class SortCombinedFieldTest(DummyDataTestCase):
|
||||
def test_computed_first(self):
|
||||
q = ''
|
||||
s1 = dbcore.query.ComputedFieldSort(beets.library.Album, "path", True)
|
||||
s2 = dbcore.query.FixedFieldSort("year", True)
|
||||
sort = dbcore.query.MultipleSort()
|
||||
sort.add_criteria(s1)
|
||||
sort.add_criteria(s2)
|
||||
results = self.lib.albums(q, sort)
|
||||
self.assertLessEqual(results[0]['path'], results[1]['path'])
|
||||
self.assertLessEqual(results[1]['path'], results[2]['path'])
|
||||
q = 'path+ year+'
|
||||
results2 = self.lib.albums(q)
|
||||
for r1, r2 in zip(results, results2):
|
||||
self.assertEqual(r1.id, r2.id)
|
||||
|
||||
def test_computed_second(self):
|
||||
q = ''
|
||||
s1 = dbcore.query.FixedFieldSort("year", True)
|
||||
s2 = dbcore.query.ComputedFieldSort(beets.library.Album, "path", True)
|
||||
sort = dbcore.query.MultipleSort()
|
||||
sort.add_criteria(s1)
|
||||
sort.add_criteria(s2)
|
||||
results = self.lib.albums(q, sort)
|
||||
self.assertLessEqual(results[0]['year'], results[1]['year'])
|
||||
self.assertLessEqual(results[1]['year'], results[2]['year'])
|
||||
self.assertLessEqual(results[0]['path'], results[1]['path'])
|
||||
q = 'year+ path+'
|
||||
results2 = self.lib.albums(q)
|
||||
for r1, r2 in zip(results, results2):
|
||||
self.assertEqual(r1.id, r2.id)
|
||||
|
||||
|
||||
def suite():
|
||||
return unittest.TestLoader().loadTestsFromName(__name__)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(defaultTest='suite')
|
||||
Loading…
Reference in a new issue