mirror of
https://github.com/beetbox/beets.git
synced 2025-12-15 21:14:19 +01:00
Merge remote-tracking branch 'upstream/master' into mbid
This commit is contained in:
commit
c12e974852
16 changed files with 174 additions and 16 deletions
|
|
@ -86,8 +86,8 @@ class PathQuery(dbcore.FieldQuery):
|
|||
colon = query_part.find(':')
|
||||
if colon != -1:
|
||||
query_part = query_part[:colon]
|
||||
return (os.sep in query_part
|
||||
and os.path.exists(syspath(normpath(query_part))))
|
||||
return (os.sep in query_part and
|
||||
os.path.exists(syspath(normpath(query_part))))
|
||||
|
||||
def match(self, item):
|
||||
path = item.path if self.case_sensitive else item.path.lower()
|
||||
|
|
|
|||
|
|
@ -1236,7 +1236,10 @@ def show_stats(lib, query, exact):
|
|||
|
||||
for item in items:
|
||||
if exact:
|
||||
total_size += os.path.getsize(item.path)
|
||||
try:
|
||||
total_size += os.path.getsize(item.path)
|
||||
except OSError as exc:
|
||||
log.info('could not get size of {}: {}', item.path, exc)
|
||||
else:
|
||||
total_size += int(item.length * item.bitrate / 8)
|
||||
total_time += item.length
|
||||
|
|
|
|||
|
|
@ -72,9 +72,9 @@ def _logged_get(log, *args, **kwargs):
|
|||
else:
|
||||
message = 'getting URL'
|
||||
|
||||
req = requests.Request('GET', *args, **req_kwargs)
|
||||
req = requests.Request(b'GET', *args, **req_kwargs)
|
||||
with requests.Session() as s:
|
||||
s.headers = {'User-Agent': 'beets'}
|
||||
s.headers = {b'User-Agent': b'beets'}
|
||||
prepped = s.prepare_request(req)
|
||||
log.debug('{}: {}', message, prepped.url)
|
||||
return s.send(prepped, **send_kwargs)
|
||||
|
|
|
|||
|
|
@ -119,6 +119,25 @@ def print_data(data, item=None, fmt=None):
|
|||
ui.print_(lineformat.format(field, value))
|
||||
|
||||
|
||||
def print_data_keys(data, item=None):
|
||||
"""Print only the keys (field names) for an item.
|
||||
"""
|
||||
path = displayable_path(item.path) if item else None
|
||||
formatted = []
|
||||
for key, value in data.iteritems():
|
||||
formatted.append(key)
|
||||
|
||||
if len(formatted) == 0:
|
||||
return
|
||||
|
||||
line_format = u'{0}{{0}}'.format(u' ' * 4)
|
||||
if path:
|
||||
ui.print_(displayable_path(path))
|
||||
|
||||
for field in sorted(formatted):
|
||||
ui.print_(line_format.format(field))
|
||||
|
||||
|
||||
class InfoPlugin(BeetsPlugin):
|
||||
|
||||
def commands(self):
|
||||
|
|
@ -131,6 +150,8 @@ class InfoPlugin(BeetsPlugin):
|
|||
cmd.parser.add_option('-i', '--include-keys', default=[],
|
||||
action='append', dest='included_keys',
|
||||
help='comma separated list of keys to show')
|
||||
cmd.parser.add_option('-k', '--keys-only', action='store_true',
|
||||
help='show only the keys')
|
||||
cmd.parser.add_format_option(target='item')
|
||||
return [cmd]
|
||||
|
||||
|
|
@ -173,7 +194,10 @@ class InfoPlugin(BeetsPlugin):
|
|||
else:
|
||||
if not first:
|
||||
ui.print_()
|
||||
print_data(data, item, opts.format)
|
||||
if opts.keys_only:
|
||||
print_data_keys(data, item)
|
||||
else:
|
||||
print_data(data, item, opts.format)
|
||||
first = False
|
||||
|
||||
if opts.summarize:
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ New:
|
|||
* A new :doc:`/plugins/mbsubmit` lets you print the tracks of an album in a
|
||||
format parseable by MusicBrainz track parser during an interactive import
|
||||
session. :bug:`1779`
|
||||
* :doc:`/plugins/info`: A new option will print only fields' names and not
|
||||
their values. Thanks to :user:`GuilhermeHideki`. :bug:`1812`
|
||||
|
||||
.. _AcousticBrainz: http://acousticbrainz.org/
|
||||
|
||||
|
|
@ -45,6 +47,10 @@ Fixes:
|
|||
* :doc:`/plugins/lyrics`: The Genius backend has been re-enabled.
|
||||
* :doc:`/plugins/edit`: Editing metadata now moves files, when appropriate
|
||||
(like the :ref:`modify-cmd` command). :bug:`1804`
|
||||
* The :ref:`stats-cmd` command no longer crashes when files are missing or
|
||||
inaccessible. :bug:`1806`
|
||||
* :doc:`/plugins/fetchart`: Possibly fix a Unicode-related crash when using
|
||||
some versions of pyOpenSSL. :bug:`1805`
|
||||
|
||||
.. _beets.io: http://beets.io/
|
||||
.. _Beetbox: https://github.com/beetbox
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ Here's an example::
|
|||
ihate:
|
||||
warn:
|
||||
- artist:rnb
|
||||
- genre: soul
|
||||
- genre:soul
|
||||
# Only warn about tribute albums in rock genre.
|
||||
- genre:rock album:tribute
|
||||
skip:
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ Additional command-line options include:
|
|||
* ``--format`` or ``-f``: Specify a specific format with which to print every
|
||||
item. This uses the same template syntax as beets’ :doc:`path formats
|
||||
</reference/pathformat>`.
|
||||
|
||||
* ``--keys-only`` or ``-k``: Show the name of the tags without the values.
|
||||
|
||||
.. _id3v2: http://id3v2.sourceforge.net
|
||||
.. _mp3info: http://www.ibiblio.org/mp3info/
|
||||
|
|
|
|||
|
|
@ -346,3 +346,11 @@ def system_mock(name):
|
|||
yield
|
||||
finally:
|
||||
platform.system = old_system
|
||||
|
||||
|
||||
def slow_test(unused=None):
|
||||
def _id(obj):
|
||||
return obj
|
||||
if 'SKIP_SLOW_TESTS' in os.environ:
|
||||
return unittest.skip('test is slow')
|
||||
return _id
|
||||
|
|
|
|||
|
|
@ -209,10 +209,10 @@ class AAOTest(UseThePlugin):
|
|||
def test_aao_scraper_finds_image(self):
|
||||
body = b"""
|
||||
<br />
|
||||
<a href="TARGET_URL" title="View larger image"
|
||||
class="thickbox" style="color: #7E9DA2; text-decoration:none;">
|
||||
<img src="http://www.albumart.org/images/zoom-icon.jpg"
|
||||
alt="View larger image" width="17" height="15" border="0"/></a>
|
||||
<a href=\"TARGET_URL\" title=\"View larger image\"
|
||||
class=\"thickbox\" style=\"color: #7E9DA2; text-decoration:none;\">
|
||||
<img src=\"http://www.albumart.org/images/zoom-icon.jpg\"
|
||||
alt=\"View larger image\" width=\"17\" height=\"15\" border=\"0\"/></a>
|
||||
"""
|
||||
self.mock_response(self.AAO_URL, body)
|
||||
album = _common.Bag(asin=self.ASIN)
|
||||
|
|
@ -261,6 +261,7 @@ class GoogleImageTest(UseThePlugin):
|
|||
self.assertEqual(list(result_url), [])
|
||||
|
||||
|
||||
@_common.slow_test()
|
||||
class ArtImporterTest(UseThePlugin):
|
||||
def setUp(self):
|
||||
super(ArtImporterTest, self).setUp()
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ class TestHelper(helper.TestHelper):
|
|||
.format(path, tag))
|
||||
|
||||
|
||||
@_common.slow_test()
|
||||
class ImportConvertTest(unittest.TestCase, TestHelper):
|
||||
|
||||
def setUp(self):
|
||||
|
|
@ -99,6 +100,7 @@ class ImportConvertTest(unittest.TestCase, TestHelper):
|
|||
self.assertTrue(os.path.isfile(item.path))
|
||||
|
||||
|
||||
@_common.slow_test()
|
||||
class ConvertCliTest(unittest.TestCase, TestHelper):
|
||||
|
||||
def setUp(self):
|
||||
|
|
@ -186,6 +188,7 @@ class ConvertCliTest(unittest.TestCase, TestHelper):
|
|||
self.assertFalse(os.path.exists(converted))
|
||||
|
||||
|
||||
@_common.slow_test()
|
||||
class NeverConvertLossyFilesTest(unittest.TestCase, TestHelper):
|
||||
"""Test the effect of the `never_convert_lossy_files` option.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -19,8 +19,10 @@ from __future__ import (division, absolute_import, print_function,
|
|||
unicode_literals)
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
|
||||
from test import _common
|
||||
from test._common import unittest
|
||||
from beets import dbcore
|
||||
from tempfile import mkstemp
|
||||
|
|
@ -116,15 +118,28 @@ class TestDatabaseTwoModels(dbcore.Database):
|
|||
pass
|
||||
|
||||
|
||||
class TestModelWithGetters(dbcore.Model):
|
||||
|
||||
@classmethod
|
||||
def _getters(cls):
|
||||
return {'aComputedField': (lambda s: 'thing')}
|
||||
|
||||
def _template_funcs(self):
|
||||
return {}
|
||||
|
||||
|
||||
@_common.slow_test()
|
||||
class MigrationTest(unittest.TestCase):
|
||||
"""Tests the ability to change the database schema between
|
||||
versions.
|
||||
"""
|
||||
def setUp(self):
|
||||
handle, self.libfile = mkstemp('db')
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
handle, cls.orig_libfile = mkstemp('orig_db')
|
||||
os.close(handle)
|
||||
# Set up a database with the two-field schema.
|
||||
old_lib = TestDatabase2(self.libfile)
|
||||
old_lib = TestDatabase2(cls.orig_libfile)
|
||||
|
||||
# Add an item to the old library.
|
||||
old_lib._connection().execute(
|
||||
|
|
@ -133,6 +148,15 @@ class MigrationTest(unittest.TestCase):
|
|||
old_lib._connection().commit()
|
||||
del old_lib
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
os.remove(cls.orig_libfile)
|
||||
|
||||
def setUp(self):
|
||||
handle, self.libfile = mkstemp('db')
|
||||
os.close(handle)
|
||||
shutil.copyfile(self.orig_libfile, self.libfile)
|
||||
|
||||
def tearDown(self):
|
||||
os.remove(self.libfile)
|
||||
|
||||
|
|
@ -274,6 +298,40 @@ class ModelTest(unittest.TestCase):
|
|||
model2.load()
|
||||
self.assertNotIn('flex_field', model2)
|
||||
|
||||
def test_check_db_fails(self):
|
||||
with self.assertRaisesRegexp(ValueError, 'no database'):
|
||||
dbcore.Model()._check_db()
|
||||
with self.assertRaisesRegexp(ValueError, 'no id'):
|
||||
TestModel1(self.db)._check_db()
|
||||
|
||||
dbcore.Model(self.db)._check_db(need_id=False)
|
||||
|
||||
def test_missing_field(self):
|
||||
with self.assertRaises(AttributeError):
|
||||
TestModel1(self.db).nonExistingKey
|
||||
|
||||
def test_computed_field(self):
|
||||
model = TestModelWithGetters()
|
||||
self.assertEqual(model.aComputedField, 'thing')
|
||||
with self.assertRaisesRegexp(KeyError, 'computed field .+ deleted'):
|
||||
del model.aComputedField
|
||||
|
||||
def test_items(self):
|
||||
model = TestModel1(self.db)
|
||||
model.id = 5
|
||||
self.assertEqual({('id', 5), ('field_one', None)},
|
||||
set(model.items()))
|
||||
|
||||
def test_delete_internal_field(self):
|
||||
model = dbcore.Model()
|
||||
del model._db
|
||||
with self.assertRaises(AttributeError):
|
||||
model._db
|
||||
|
||||
def test_parse_nonstring(self):
|
||||
with self.assertRaisesRegexp(TypeError, "must be a string"):
|
||||
dbcore.Model._parse(None, 42)
|
||||
|
||||
|
||||
class FormatTest(unittest.TestCase):
|
||||
def test_format_fixed_field(self):
|
||||
|
|
@ -588,6 +646,15 @@ class ResultsIteratorTest(unittest.TestCase):
|
|||
objs = self.db._fetch(TestModel1)
|
||||
self.assertEqual(len(objs), 2)
|
||||
|
||||
def test_out_of_range(self):
|
||||
objs = self.db._fetch(TestModel1)
|
||||
with self.assertRaises(IndexError):
|
||||
objs[100]
|
||||
|
||||
def test_no_results(self):
|
||||
self.assertIsNone(self.db._fetch(
|
||||
TestModel1, dbcore.query.FalseQuery()).get())
|
||||
|
||||
|
||||
def suite():
|
||||
return unittest.TestLoader().loadTestsFromName(__name__)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from __future__ import (division, absolute_import, print_function,
|
|||
import codecs
|
||||
|
||||
from mock import patch
|
||||
from test import _common
|
||||
from test._common import unittest
|
||||
from test.helper import TestHelper, control_stdin
|
||||
|
||||
|
|
@ -62,6 +63,7 @@ class ModifyFileMocker(object):
|
|||
f.write(contents)
|
||||
|
||||
|
||||
@_common.slow_test()
|
||||
class EditCommandTest(unittest.TestCase, TestHelper):
|
||||
""" Black box tests for `beetsplug.edit`. Command line interaction is
|
||||
simulated using `test.helper.control_stdin()`, and yaml editing via an
|
||||
|
|
|
|||
|
|
@ -236,6 +236,7 @@ class ImportHelper(TestHelper):
|
|||
self.assertEqual(len(os.listdir(self.libdir)), 0)
|
||||
|
||||
|
||||
@_common.slow_test()
|
||||
class NonAutotaggedImportTest(_common.TestCase, ImportHelper):
|
||||
def setUp(self):
|
||||
self.setup_beets(disk=True)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from StringIO import StringIO
|
|||
import beets.logging as blog
|
||||
from beets import plugins, ui
|
||||
import beetsplug
|
||||
from test import _common
|
||||
from test._common import unittest, TestCase
|
||||
from test import helper
|
||||
|
||||
|
|
@ -163,6 +164,7 @@ class LoggingLevelTest(unittest.TestCase, helper.TestHelper):
|
|||
self.assertIn('dummy: debug import_stage', logs)
|
||||
|
||||
|
||||
@_common.slow_test()
|
||||
class ConcurrentEventsTest(TestCase, helper.TestHelper):
|
||||
"""Similar to LoggingLevelTest but lower-level and focused on multiple
|
||||
events interaction. Since this is a bit heavy we don't do it in
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ from __future__ import (division, absolute_import, print_function,
|
|||
unicode_literals)
|
||||
|
||||
|
||||
from mock import Mock
|
||||
from mock import Mock, patch, call, ANY
|
||||
from test._common import unittest
|
||||
from test.helper import TestHelper
|
||||
|
||||
|
|
@ -44,6 +44,44 @@ class MPDStatsTest(unittest.TestCase, TestHelper):
|
|||
self.assertFalse(mpdstats.update_rating(item, True))
|
||||
self.assertFalse(mpdstats.update_rating(None, True))
|
||||
|
||||
def test_get_item(self):
|
||||
ITEM_PATH = '/foo/bar.flac'
|
||||
item = Item(title='title', path=ITEM_PATH, id=1)
|
||||
item.add(self.lib)
|
||||
|
||||
log = Mock()
|
||||
mpdstats = MPDStats(self.lib, log)
|
||||
|
||||
self.assertEqual(str(mpdstats.get_item(ITEM_PATH)), str(item))
|
||||
self.assertIsNone(mpdstats.get_item('/some/non-existing/path'))
|
||||
self.assertIn('item not found:', log.info.call_args[0][0])
|
||||
|
||||
FAKE_UNKNOWN_STATE = 'some-unknown-one'
|
||||
STATUSES = [{'state': FAKE_UNKNOWN_STATE},
|
||||
{'state': 'pause'},
|
||||
{'state': 'play', 'songid': 1, 'time': '0:1'},
|
||||
{'state': 'stop'}]
|
||||
EVENTS = [["player"]] * (len(STATUSES) - 1) + [KeyboardInterrupt]
|
||||
ITEM_PATH = '/foo/bar.flac'
|
||||
|
||||
@patch("beetsplug.mpdstats.MPDClientWrapper", return_value=Mock(**{
|
||||
"events.side_effect": EVENTS, "status.side_effect": STATUSES,
|
||||
"playlist.return_value": {1: ITEM_PATH}}))
|
||||
def test_run_MPDStats(self, mpd_mock):
|
||||
item = Item(title='title', path=self.ITEM_PATH, id=1)
|
||||
item.add(self.lib)
|
||||
|
||||
log = Mock()
|
||||
try:
|
||||
MPDStats(self.lib, log).run()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
log.debug.assert_has_calls(
|
||||
[call(u'unhandled status "{0}"', ANY)])
|
||||
log.info.assert_has_calls(
|
||||
[call(u'pause'), call(u'playing {0}', ANY), call(u'stop')])
|
||||
|
||||
|
||||
def suite():
|
||||
return unittest.TestLoader().loadTestsFromName(__name__)
|
||||
|
|
|
|||
|
|
@ -594,6 +594,7 @@ class InputTest(_common.TestCase):
|
|||
self.assertEqual(album, u'\xc2me')
|
||||
|
||||
|
||||
@_common.slow_test()
|
||||
class ConfigTest(unittest.TestCase, TestHelper):
|
||||
def setUp(self):
|
||||
self.setup_beets()
|
||||
|
|
@ -1035,6 +1036,7 @@ class PathFormatTest(_common.TestCase):
|
|||
self.assertEqual(pf[1:], default_formats)
|
||||
|
||||
|
||||
@_common.slow_test()
|
||||
class PluginTest(_common.TestCase):
|
||||
def test_plugin_command_from_pluginpath(self):
|
||||
config['pluginpath'] = [os.path.join(_common.RSRC, 'beetsplug')]
|
||||
|
|
@ -1042,6 +1044,7 @@ class PluginTest(_common.TestCase):
|
|||
ui._raw_main(['test'])
|
||||
|
||||
|
||||
@_common.slow_test()
|
||||
class CompletionTest(_common.TestCase):
|
||||
def test_completion(self):
|
||||
# Load plugin commands
|
||||
|
|
|
|||
Loading…
Reference in a new issue