diff --git a/NEWS b/NEWS index be7750c8a..6afef5c27 100644 --- a/NEWS +++ b/NEWS @@ -62,6 +62,7 @@ reuse by plugins (e.g., the FUSE plugin). * Singleton imports ("beet import -s") can now take individual files as arguments as well as directories. +* Fix Unicode queries given on the command line. * Fix crasher in quiet singleton imports (import -qs). * Fix crash when autotagging files with no metadata. * Fix a rare deadlock when finishing the import pipeline. diff --git a/beets/ui/__init__.py b/beets/ui/__init__.py index 988560415..cc4b7d50d 100644 --- a/beets/ui/__init__.py +++ b/beets/ui/__init__.py @@ -52,6 +52,21 @@ class UserError(Exception): # Utilities. +def _encoding(): + """Tries to guess the encoding uses by the terminal.""" + try: + return locale.getdefaultlocale()[1] or 'utf8' + except ValueError: + # Invalid locale environment variable setting. To avoid + # failing entirely for no good reason, assume UTF-8. + return 'utf8' + +def decargs(arglist): + """Given a list of command-line argument bytestrings, attempts to + decode them to Unicode strings. + """ + return [s.decode(_encoding()) for s in arglist] + def print_(*strings): """Like print, but rather than raising an error when a character is not in the terminal's encoding's character set, just silently @@ -65,13 +80,7 @@ def print_(*strings): else: txt = u'' if isinstance(txt, unicode): - try: - encoding = locale.getdefaultlocale()[1] or 'utf8' - except ValueError: - # Invalid locale environment variable setting. To avoid - # failing entirely for no good reason, assume UTF-8. - encoding = 'utf8' - txt = txt.encode(encoding, 'replace') + txt = txt.encode(_encoding(), 'replace') print txt def input_options(options, require=False, prompt=None, fallback_prompt=None, diff --git a/beets/ui/commands.py b/beets/ui/commands.py index 47f299f25..080cade5f 100755 --- a/beets/ui/commands.py +++ b/beets/ui/commands.py @@ -22,7 +22,7 @@ import os import time from beets import ui -from beets.ui import print_ +from beets.ui import print_, decargs from beets import autotag import beets.autotag.art from beets import plugins @@ -622,7 +622,7 @@ list_cmd.parser.add_option('-a', '--album', action='store_true', list_cmd.parser.add_option('-p', '--path', action='store_true', help='print paths for matched items or albums') def list_func(lib, config, opts, args): - list_items(lib, args, opts.album, opts.path) + list_items(lib, decargs(args), opts.album, opts.path) list_cmd.func = list_func default_commands.append(list_cmd) @@ -677,7 +677,7 @@ remove_cmd.parser.add_option("-d", "--delete", action="store_true", remove_cmd.parser.add_option('-a', '--album', action='store_true', help='match albums instead of tracks') def remove_func(lib, config, opts, args): - remove_items(lib, args, opts.album, opts.delete) + remove_items(lib, decargs(args), opts.album, opts.delete) remove_cmd.func = remove_func default_commands.append(remove_cmd) @@ -718,7 +718,7 @@ Albums: %i""" % ( stats_cmd = ui.Subcommand('stats', help='show statistics about the library or a query') def stats_func(lib, config, opts, args): - show_stats(lib, args) + show_stats(lib, decargs(args)) stats_cmd.func = stats_func default_commands.append(stats_cmd) diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 8d5e4794f..6f4c8c250 100755 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -4,6 +4,7 @@ import imghdr from beets.plugins import BeetsPlugin from beets import mediafile from beets import ui +from beets.ui import decargs from beets.util import syspath, normpath log = logging.getLogger('beets') @@ -41,7 +42,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): if not args: raise ui.UserError('specify an image file') imagepath = normpath(args.pop(0)) - embed(lib, imagepath, args) + embed(lib, imagepath, decargs(args)) embed_cmd.func = embed_func # Extract command. @@ -51,14 +52,14 @@ class EmbedCoverArtPlugin(BeetsPlugin): help='image output file') def extract_func(lib, config, opts, args): outpath = normpath(opts.outpath or 'cover') - extract(lib, outpath, args) + extract(lib, outpath, decargs(args)) extract_cmd.func = extract_func # Clear command. clear_cmd = ui.Subcommand('clearart', help='remove images from file metadata') def clear_func(lib, config, opts, args): - clear(lib, args) + clear(lib, decargs(args)) clear_cmd.func = clear_func return [embed_cmd, extract_cmd, clear_cmd] diff --git a/test/_common.py b/test/_common.py index 4905ebc04..f2af539ad 100644 --- a/test/_common.py +++ b/test/_common.py @@ -1,4 +1,3 @@ -"""Some common functionality for beets' test cases.""" # This file is part of beets. # Copyright 2011, Adrian Sampson. # @@ -13,6 +12,7 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. +"""Some common functionality for beets' test cases.""" import time import sys import os diff --git a/test/test_query.py b/test/test_query.py index f78c14e69..cc2a9225c 100644 --- a/test/test_query.py +++ b/test/test_query.py @@ -215,6 +215,16 @@ class MemoryGetTest(unittest.TestCase, AssertsMixin): names = [a.album for a in results] self.assertEqual(names, ['the album']) + def test_unicode_query(self): + self.single_item.title = u'caf\xe9' + self.lib.store(self.single_item) + self.lib.save() + + q = u'title:caf\xe9' + results = self.lib.items(q) + self.assert_matched(results, u'caf\xe9') + self.assert_done(results) + class PathQueryTest(unittest.TestCase, AssertsMixin): def setUp(self): self.lib = beets.library.Library(':memory:') diff --git a/test/test_ui.py b/test/test_ui.py index fa61da0cb..99de272a6 100644 --- a/test/test_ui.py +++ b/test/test_ui.py @@ -37,6 +37,7 @@ class ListTest(unittest.TestCase): i.path = 'xxx/yyy' self.lib.add(i) self.lib.add_album([i]) + self.item = i def tearDown(self): self.io.restore() @@ -46,6 +47,15 @@ class ListTest(unittest.TestCase): out = self.io.getoutput() self.assertTrue(u'the title' in out) + def test_list_unicode_query(self): + self.item.title = u'na\xefve' + self.lib.store(self.item) + self.lib.save() + + commands.list_items(self.lib, [u'na\xefve'], False, False) + out = self.io.getoutput() + self.assertTrue(u'na\xefve' in out.decode(self.io.stdout.encoding)) + def test_list_item_path(self): commands.list_items(self.lib, '', False, True) out = self.io.getoutput()