This commit is contained in:
e5e4eaeacd39c5cfba4d7c852c48277ae50331e6 2014-08-23 11:00:23 +10:00
commit bd3f1ca5c0
18 changed files with 858 additions and 83 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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)

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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
View 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')