Merge pull request #1269 from brunal/libmodels-formatting

Automatic formatting for Album & Item
This commit is contained in:
Adrian Sampson 2015-01-26 15:12:06 -08:00
commit 94020963d2
16 changed files with 98 additions and 112 deletions

View file

@ -229,6 +229,8 @@ class WriteError(FileOperationError):
class LibModel(dbcore.Model):
"""Shared concrete functionality for Items and Albums.
"""
_format_config_key = None
"""Config key that specifies how an instance should be formatted"""
def _template_funcs(self):
funcs = DefaultTemplateFunctions(self, self._db).functions()
@ -247,6 +249,22 @@ class LibModel(dbcore.Model):
super(LibModel, self).add(lib)
plugins.send('database_change', lib=self._db)
def __format__(self, spec):
if not spec:
spec = beets.config[self._format_config_key].get(unicode)
result = self.evaluate_template(spec)
if isinstance(spec, bytes):
# if spec is a byte string then we must return a one as well
return result.encode('utf8')
else:
return result
def __str__(self):
return format(self).encode('utf8')
def __unicode__(self):
return format(self)
class FormattedItemMapping(dbcore.db.FormattedMapping):
"""Add lookup for album-level fields.
@ -383,6 +401,8 @@ class Item(LibModel):
_sorts = {'artist': SmartArtistSort}
_format_config_key = 'list_format_item'
@classmethod
def _getters(cls):
getters = plugins.item_field_getters()
@ -789,6 +809,8 @@ class Album(LibModel):
"""List of keys that are set on an album's items.
"""
_format_config_key = 'list_format_album'
@classmethod
def _getters(cls):
# In addition to plugin-provided computed fields, also expose

View file

@ -97,6 +97,9 @@ 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
replaces it.
If the arguments are strings then they're expected to share the same type:
either bytes or unicode.
"""
if strings:
if isinstance(strings[0], unicode):
@ -471,31 +474,6 @@ def get_replacements():
return replacements
def _pick_format(album, fmt=None):
"""Pick a format string for printing Album or Item objects,
falling back to config options and defaults.
"""
if fmt:
return fmt
if album:
return config['list_format_album'].get(unicode)
else:
return config['list_format_item'].get(unicode)
def print_obj(obj, lib, fmt=None):
"""Print an Album or Item object. If `fmt` is specified, use that
format string. Otherwise, use the configured template.
"""
album = isinstance(obj, library.Album)
fmt = _pick_format(album, fmt)
if isinstance(fmt, Template):
template = fmt
else:
template = Template(fmt)
print_(obj.evaluate_template(template))
def term_width():
"""Get the width (columns) of the terminal."""
fallback = config['ui']['terminal_width'].get(int)
@ -587,7 +565,7 @@ def show_model_changes(new, old=None, fields=None, always=False):
# Print changes.
if changes or always:
print_obj(old, old._db)
print_(format(old))
if changes:
print_(u'\n'.join(changes))

View file

@ -32,7 +32,6 @@ from beets import plugins
from beets import importer
from beets import util
from beets.util import syspath, normpath, ancestry, displayable_path
from beets.util.functemplate import Template
from beets import library
from beets import config
from beets import logging
@ -951,20 +950,16 @@ def list_items(lib, query, album, fmt):
"""Print out items in lib matching query. If album, then search for
albums instead of single items.
"""
tmpl = Template(ui._pick_format(album, fmt))
if album:
for album in lib.albums(query):
ui.print_obj(album, lib, tmpl)
ui.print_(format(album, fmt))
else:
for item in lib.items(query):
ui.print_obj(item, lib, tmpl)
ui.print_(format(item, fmt))
def list_func(lib, opts, args):
if opts.path:
fmt = '$path'
else:
fmt = opts.format
fmt = '$path' if opts.path else opts.format
list_items(lib, decargs(args), opts.album, fmt)
@ -979,7 +974,7 @@ list_cmd.parser.add_option(
)
list_cmd.parser.add_option(
'-f', '--format', action='store',
help='print with custom format', default=None
help='print with custom format', default=''
)
list_cmd.func = list_func
default_commands.append(list_cmd)
@ -999,7 +994,7 @@ def update_items(lib, query, album, move, pretend):
for item in items:
# Item deleted?
if not os.path.exists(syspath(item.path)):
ui.print_obj(item, lib)
ui.print_(format(item))
ui.print_(ui.colorize('red', u' deleted'))
if not pretend:
item.remove(True)
@ -1095,7 +1090,7 @@ update_cmd.parser.add_option(
)
update_cmd.parser.add_option(
'-f', '--format', action='store',
help='print with custom format', default=None
help='print with custom format', default=''
)
update_cmd.func = update_func
default_commands.append(update_cmd)
@ -1116,13 +1111,13 @@ def remove_items(lib, query, album, delete):
fmt = u'$path - $title'
prompt = 'Really DELETE %i files (y/n)?' % len(items)
else:
fmt = None
fmt = ''
prompt = 'Really remove %i items from the library (y/n)?' % \
len(items)
# Show all the items.
for item in items:
ui.print_obj(item, lib, fmt)
ui.print_(format(item, fmt))
# Confirm with user.
if not ui.input_yn(prompt, True):
@ -1352,7 +1347,7 @@ modify_cmd.parser.add_option(
)
modify_cmd.parser.add_option(
'-f', '--format', action='store',
help='print with custom format', default=None
help='print with custom format', default=''
)
modify_cmd.func = modify_func
default_commands.append(modify_cmd)

View file

@ -356,7 +356,7 @@ class ConvertPlugin(BeetsPlugin):
self.config['pretend'].get(bool)
if not pretend:
ui.commands.list_items(lib, ui.decargs(args), opts.album, None)
ui.commands.list_items(lib, ui.decargs(args), opts.album, '')
if not (opts.yes or ui.input_yn("Convert? (Y/n)")):
return

View file

@ -17,14 +17,14 @@
import shlex
from beets.plugins import BeetsPlugin
from beets.ui import decargs, print_obj, vararg_callback, Subcommand, UserError
from beets.ui import decargs, print_, vararg_callback, Subcommand, UserError
from beets.util import command_output, displayable_path, subprocess
PLUGIN = 'duplicates'
def _process_item(item, lib, copy=False, move=False, delete=False,
tag=False, format=None):
tag=False, format=''):
"""Process Item `item` in `lib`.
"""
if copy:
@ -42,7 +42,7 @@ def _process_item(item, lib, copy=False, move=False, delete=False,
raise UserError('%s: can\'t parse k=v tag: %s' % (PLUGIN, tag))
setattr(k, v)
item.store()
print_obj(item, lib, fmt=format)
print_(format(item, format))
def _checksum(item, prog, log):
@ -126,7 +126,7 @@ class DuplicatesPlugin(BeetsPlugin):
self._command.parser.add_option('-f', '--format', dest='format',
action='store', type='string',
help='print with custom format',
metavar='FMT')
metavar='FMT', default='')
self._command.parser.add_option('-a', '--album', dest='album',
action='store_true',

View file

@ -115,7 +115,7 @@ def similar(lib, src_item, threshold=0.15, fmt='${difference}: ${path}'):
d = diff(item, src_item)
if d < threshold:
s = fmt.replace('${difference}', '{:2.2f}'.format(d))
ui.print_obj(item, lib, s)
ui.print_(format(item, s))
class EchonestMetadataPlugin(plugins.BeetsPlugin):
@ -401,10 +401,9 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin):
for method in methods:
song = method(item)
if song:
self._log.debug(u'got song through {0}: {1} - {2} [{3}]',
self._log.debug(u'got song through {0}: {1} [{2}]',
method.__name__,
item.artist,
item.title,
item,
song.get('duration'),
)
return song
@ -471,7 +470,7 @@ class EchonestMetadataPlugin(plugins.BeetsPlugin):
self.config.set_args(opts)
write = config['import']['write'].get(bool)
for item in lib.items(ui.decargs(args)):
self._log.info(u'{0} - {1}', item.artist, item.title)
self._log.info(u'{0}', item)
if self.config['force'] or self.requires_update(item):
song = self.fetch_song(item)
if song:

View file

@ -26,12 +26,6 @@ from beets.ui import decargs
from beets.util import syspath, normpath, displayable_path
from beets.util.artresizer import ArtResizer
from beets import config
from beets.util.functemplate import Template
__item_template = Template(ui._pick_format(False))
fmt_item = lambda item: item.evaluate_template(__item_template)
__album_template = Template(ui._pick_format(True))
fmt_album = lambda item: item.evaluate_template(__album_template)
class EmbedCoverArtPlugin(BeetsPlugin):
@ -146,16 +140,16 @@ class EmbedCoverArtPlugin(BeetsPlugin):
"""
imagepath = album.artpath
if not imagepath:
self._log.info(u'No album art present for {0}', fmt_album(album))
self._log.info(u'No album art present for {0}', album)
return
if not os.path.isfile(syspath(imagepath)):
self._log.info(u'Album art not found at {0} for {1}',
displayable_path(imagepath), fmt_album(album))
displayable_path(imagepath), album)
return
if maxwidth:
imagepath = self.resize_image(imagepath, maxwidth)
self._log.info(u'Embedding album art into {0}', fmt_album(album))
self._log.info(u'Embedding album art into {0}', album)
for item in album.items():
thresh = self.config['compare_threshold'].get(int)
@ -244,8 +238,7 @@ class EmbedCoverArtPlugin(BeetsPlugin):
art = self.get_art(item)
if not art:
self._log.info(u'No album art present in {0}, skipping.',
fmt_item(item))
self._log.info(u'No album art present in {0}, skipping.', item)
return
# Add an extension to the filename.
@ -257,7 +250,7 @@ class EmbedCoverArtPlugin(BeetsPlugin):
outpath += '.' + ext
self._log.info(u'Extracting album art from: {0} to: {1}',
fmt_item(item), displayable_path(outpath))
item, displayable_path(outpath))
with open(syspath(outpath), 'wb') as f:
f.write(art)
return outpath
@ -269,7 +262,7 @@ class EmbedCoverArtPlugin(BeetsPlugin):
items = lib.items(query)
self._log.info(u'Clearing album art from {0} items', len(items))
for item in items:
self._log.debug(u'Clearing art for {0}', fmt_item(item))
self._log.debug(u'Clearing art for {0}', item)
try:
mf = mediafile.MediaFile(syspath(item.path), id3v23)
except mediafile.UnreadableFileError as exc:

View file

@ -448,7 +448,7 @@ class FetchArtPlugin(plugins.BeetsPlugin):
else:
message = ui.colorize('red', 'no art found')
self._log.info(u'{0.albumartist} - {0.album}: {1}', album, message)
self._log.info(u'{0}: {1}', album, message)
def _source_urls(self, album):
"""Generate possible source URLs for an album's art. The URLs are

View file

@ -337,8 +337,8 @@ class LastGenrePlugin(plugins.BeetsPlugin):
for album in lib.albums(ui.decargs(args)):
album.genre, src = self._get_genre(album)
self._log.info(u'genre for album {0.albumartist} - {0.album} '
u'({1}): {0.genre}', album, src)
self._log.info(u'genre for album {0} ({1}): {0.genre}',
album, src)
album.store()
for item in album.items():
@ -347,8 +347,8 @@ class LastGenrePlugin(plugins.BeetsPlugin):
if 'track' in self.sources:
item.genre, src = self._get_genre(item)
item.store()
self._log.info(u'genre for track {0.artist} - {0.tit'
u'le} ({1}): {0.genre}', item, src)
self._log.info(u'genre for track {0} ({1}): {0.genre}',
item, src)
if write:
item.try_write()

View file

@ -508,8 +508,7 @@ class LyricsPlugin(plugins.BeetsPlugin):
lyrics will also be written to the file itself."""
# Skip if the item already has lyrics.
if not force and item.lyrics:
self._log.info(u'lyrics already present: {0.artist} - {0.title}',
item)
self._log.info(u'lyrics already present: {0}', item)
return
lyrics = None
@ -521,9 +520,9 @@ class LyricsPlugin(plugins.BeetsPlugin):
lyrics = u"\n\n---\n\n".join([l for l in lyrics if l])
if lyrics:
self._log.info(u'fetched lyrics: {0.artist} - {0.title}', item)
self._log.info(u'fetched lyrics: {0}', item)
else:
self._log.info(u'lyrics not found: {0.artist} - {0.title}', item)
self._log.info(u'lyrics not found: {0}', item)
fallback = self.config['fallback'].get()
if fallback:
lyrics = fallback

View file

@ -18,7 +18,6 @@ from beets.plugins import BeetsPlugin
from beets import autotag, library, ui, util
from beets.autotag import hooks
from beets import config
from beets.util.functemplate import Template
from collections import defaultdict
@ -50,7 +49,7 @@ class MBSyncPlugin(BeetsPlugin):
cmd.parser.add_option('-W', '--nowrite', action='store_false',
default=config['import']['write'], dest='write',
help="don't write updated metadata to files")
cmd.parser.add_option('-f', '--format', action='store', default=None,
cmd.parser.add_option('-f', '--format', action='store', default='',
help='print with custom format')
cmd.func = self.func
return [cmd]
@ -71,10 +70,8 @@ class MBSyncPlugin(BeetsPlugin):
"""Retrieve and apply info from the autotagger for items matched by
query.
"""
template = Template(ui._pick_format(False, fmt))
for item in lib.items(query + ['singleton:true']):
item_formatted = item.evaluate_template(template)
item_formatted = format(item, fmt)
if not item.mb_trackid:
self._log.info(u'Skipping singleton with no mb_trackid: {0}',
item_formatted)
@ -97,11 +94,9 @@ class MBSyncPlugin(BeetsPlugin):
"""Retrieve and apply info from the autotagger for albums matched by
query and their items.
"""
template = Template(ui._pick_format(True, fmt))
# Process matching albums.
for a in lib.albums(query):
album_formatted = a.evaluate_template(template)
album_formatted = format(a, fmt)
if not a.mb_albumid:
self._log.info(u'Skipping album with no mb_albumid: {0}',
album_formatted)

View file

@ -17,7 +17,7 @@
from beets.autotag import hooks
from beets.library import Item
from beets.plugins import BeetsPlugin
from beets.ui import decargs, print_obj, Subcommand
from beets.ui import decargs, print_, Subcommand
def _missing_count(album):
@ -95,7 +95,7 @@ class MissingPlugin(BeetsPlugin):
self._command.parser.add_option('-f', '--format', dest='format',
action='store', type='string',
help='print with custom FORMAT',
metavar='FORMAT')
metavar='FORMAT', default='')
self._command.parser.add_option('-c', '--count', dest='count',
action='store_true',
@ -123,13 +123,12 @@ class MissingPlugin(BeetsPlugin):
for album in albums:
if count:
missing = _missing_count(album)
if missing:
print_obj(album, lib, fmt=fmt)
if _missing_count(album):
print_(format(album, fmt))
else:
for item in self._missing(album):
print_obj(item, lib, fmt=fmt)
print_(format(item, fmt))
self._command.func = _miss
return [self._command]

View file

@ -16,8 +16,7 @@
"""
from __future__ import absolute_import
from beets.plugins import BeetsPlugin
from beets.ui import Subcommand, decargs, print_obj
from beets.util.functemplate import Template
from beets.ui import Subcommand, decargs, print_
import random
from operator import attrgetter
from itertools import groupby
@ -25,11 +24,7 @@ from itertools import groupby
def random_item(lib, opts, args):
query = decargs(args)
if opts.path:
fmt = '$path'
else:
fmt = opts.format
template = Template(fmt) if fmt else None
fmt = '$path' if opts.path else opts.format
if opts.album:
objs = list(lib.albums(query))
@ -66,7 +61,7 @@ def random_item(lib, opts, args):
objs = random.sample(objs, number)
for item in objs:
print_obj(item, lib, template)
print_(format(item, fmt))
random_cmd = Subcommand('random',
help='chose a random track or album')
@ -75,7 +70,7 @@ random_cmd.parser.add_option('-a', '--album', action='store_true',
random_cmd.parser.add_option('-p', '--path', action='store_true',
help='print the path of the matched item')
random_cmd.parser.add_option('-f', '--format', action='store',
help='print with custom format', default=None)
help='print with custom format', default='')
random_cmd.parser.add_option('-n', '--number', action='store', type="int",
help='number of objects to choose', default=1)
random_cmd.parser.add_option('-e', '--equal-chance', action='store_true',

View file

@ -558,7 +558,7 @@ class AudioToolsBackend(Backend):
:rtype: :class:`AlbumGain`
"""
self._log.debug(u'Analysing album {0.albumartist} - {0.album}', album)
self._log.debug(u'Analysing album {0}', album)
# The first item is taken and opened to get the sample rate to
# initialize the replaygain object. The object is used for all the
@ -574,15 +574,13 @@ class AudioToolsBackend(Backend):
track_gains.append(
Gain(gain=rg_track_gain, peak=rg_track_peak)
)
self._log.debug(u'ReplayGain for track {0.artist} - {0.title}: '
u'{1:.2f}, {2:.2f}',
self._log.debug(u'ReplayGain for track {0}: {1:.2f}, {2:.2f}',
item, rg_track_gain, rg_track_peak)
# After getting the values for all tracks, it's possible to get the
# album values.
rg_album_gain, rg_album_peak = rg.album_gain()
self._log.debug(u'ReplayGain for album {0.albumartist} - {0.album}: '
u'{1:.2f}, {2:.2f}',
self._log.debug(u'ReplayGain for album {0}: {1:.2f}, {2:.2f}',
album, rg_album_gain, rg_album_peak)
return AlbumGain(
@ -674,20 +672,17 @@ class ReplayGainPlugin(BeetsPlugin):
items, nothing is done.
"""
if not self.album_requires_gain(album):
self._log.info(u'Skipping album {0} - {1}',
album.albumartist, album.album)
self._log.info(u'Skipping album {0}', album)
return
self._log.info(u'analyzing {0} - {1}', album.albumartist, album.album)
self._log.info(u'analyzing {0}', album)
try:
album_gain = self.backend_instance.compute_album_gain(album)
if len(album_gain.track_gains) != len(album.items()):
raise ReplayGainError(
u"ReplayGain backend failed "
u"for some tracks in album {0} - {1}".format(
album.albumartist, album.album
)
u"for some tracks in album {0}".format(album)
)
self.store_album_gain(album, album_gain.album_gain)
@ -711,18 +706,16 @@ class ReplayGainPlugin(BeetsPlugin):
in the item, nothing is done.
"""
if not self.track_requires_gain(item):
self._log.info(u'Skipping track {0.artist} - {0.title}', item)
self._log.info(u'Skipping track {0}', item)
return
self._log.info(u'analyzing {0} - {1}', item.artist, item.title)
self._log.info(u'analyzing {0}', item)
try:
track_gains = self.backend_instance.compute_track_gain([item])
if len(track_gains) != 1:
raise ReplayGainError(
u"ReplayGain backend failed for track {0} - {1}".format(
item.artist, item.title
)
u"ReplayGain backend failed for track {0}".format(item)
)
self.store_track_gain(item, track_gains[0])

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
# This file is part of beets.
# Copyright 2015, Adrian Sampson.
#
@ -1078,6 +1079,23 @@ class TemplateTest(_common.LibTestCase):
self.album.store()
self.assertEqual(self.i.evaluate_template('$foo'), 'baz')
def test_album_and_item_format(self):
config['list_format_album'] = u'foö $foo'
album = beets.library.Album()
album.foo = 'bar'
album.tagada = 'togodo'
self.assertEqual(u"{0}".format(album), u"foö bar")
self.assertEqual(u"{0:$tagada}".format(album), u"togodo")
self.assertEqual(unicode(album), u"foö bar")
self.assertEqual(str(album), b"fo\xc3\xb6 bar")
config['list_format_item'] = 'bar $foo'
item = beets.library.Item()
item.foo = 'bar'
item.tagada = 'togodo'
self.assertEqual("{0}".format(item), "bar bar")
self.assertEqual("{0:$tagada}".format(item), "togodo")
class UnicodePathTest(_common.LibTestCase):
def test_unicode_path(self):

View file

@ -43,7 +43,7 @@ class ListTest(unittest.TestCase):
self.lib.add(self.item)
self.lib.add_album([self.item])
def _run_list(self, query='', album=False, path=False, fmt=None):
def _run_list(self, query='', album=False, path=False, fmt=''):
commands.list_items(self.lib, query, album, fmt)
def test_list_outputs_item(self):