Merge branch 'master' of github.com:sampsyo/beets into imenem-discogs-search

Conflicts:
	docs/changelog.rst
	docs/plugins/index.rst
This commit is contained in:
Tai Lee 2013-05-21 23:27:53 +10:00
commit 75a4171d20
24 changed files with 534 additions and 99 deletions

View file

@ -3,8 +3,8 @@ python:
- "2.7"
- "2.6"
install:
- pip install . --use-mirrors
- pip install pylast flask --use-mirrors
- travis_retry pip install . --use-mirrors
- travis_retry pip install pylast flask --use-mirrors
- "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2 --use-mirrors; fi"
script: nosetests
branches:

View file

@ -20,4 +20,4 @@ from beets.util import confit
Library = beets.library.Library
config = confit.Configuration('beets', __name__, False)
config = confit.LazyConfig('beets', __name__)

View file

@ -196,12 +196,12 @@ def format_for_path(value, key=None, pathmod=None):
pathmod = pathmod or os.path
if isinstance(value, basestring):
if isinstance(value, str):
value = value.decode('utf8', 'ignore')
sep_repl = beets.config['path_sep_replace'].get(unicode)
for sep in (pathmod.sep, pathmod.altsep):
if sep:
value = value.replace(
sep,
beets.config['path_sep_replace'].get(unicode),
)
value = value.replace(sep, sep_repl)
elif key in ('track', 'tracktotal', 'disc', 'disctotal'):
# Pad indices with zeros.
value = u'%02i' % (value or 0)
@ -1113,8 +1113,6 @@ class Library(BaseLibrary):
directory='~/Music',
path_formats=((PF_KEY_DEFAULT,
'$artist/$album/$track $title'),),
art_filename='cover',
timeout=5.0,
replacements=None,
item_fields=ITEM_FIELDS,
album_fields=ALBUM_FIELDS):
@ -1124,12 +1122,10 @@ class Library(BaseLibrary):
self.path = bytestring_path(normpath(path))
self.directory = bytestring_path(normpath(directory))
self.path_formats = path_formats
self.art_filename = art_filename
self.replacements = replacements
self._memotable = {} # Used for template substitution performance.
self.timeout = timeout
self._connections = {}
self._tx_stacks = defaultdict(list)
# A lock to protect the _connections and _tx_stacks maps, which
@ -1210,7 +1206,10 @@ class Library(BaseLibrary):
return self._connections[thread_id]
else:
# Make a new connection.
conn = sqlite3.connect(self.path, timeout=self.timeout)
conn = sqlite3.connect(
self.path,
timeout=beets.config['timeout'].as_number(),
)
# Access SELECT results like dictionaries.
conn.row_factory = sqlite3.Row
@ -1703,12 +1702,10 @@ class Album(BaseAlbum):
image = bytestring_path(image)
item_dir = item_dir or self.item_dir()
if not isinstance(self._library.art_filename,Template):
self._library.art_filename = Template(self._library.art_filename)
subpath = util.sanitize_path(format_for_path(
self.evaluate_template(self._library.art_filename)
))
filename_tmpl = Template(beets.config['art_filename'].get(unicode))
subpath = format_for_path(self.evaluate_template(filename_tmpl))
subpath = util.sanitize_path(subpath,
replacements=self._library.replacements)
subpath = bytestring_path(subpath)
_, ext = os.path.splitext(image)
@ -1754,6 +1751,10 @@ class Album(BaseAlbum):
mapping['artpath'] = displayable_path(mapping['artpath'])
mapping['path'] = displayable_path(self.item_dir())
# Get values from plugins.
for key, value in plugins.template_values(self).iteritems():
mapping[key] = value
# Get template functions.
funcs = DefaultTemplateFunctions().functions()
funcs.update(plugins.template_funcs())

View file

@ -689,18 +689,13 @@ class SubcommandsOptionParser(optparse.OptionParser):
# The root parser and its main function.
def _raw_main(args, load_config=True):
def _raw_main(args):
"""A helper function for `main` without top-level exception
handling.
"""
# Load global configuration files.
if load_config:
config.read()
# Temporary: Migrate from 1.0-style configuration.
from beets.ui import migrate
if load_config:
migrate.automigrate()
migrate.automigrate()
# Get the default subcommands.
from beets.ui.commands import default_commands
@ -734,8 +729,6 @@ def _raw_main(args, load_config=True):
dbpath,
config['directory'].as_filename(),
get_path_formats(),
Template(config['art_filename'].get(unicode)),
config['timeout'].as_number(),
get_replacements(),
)
except sqlite3.OperationalError:

View file

@ -91,12 +91,21 @@ def _showdiff(field, oldval, newval):
fields_cmd = ui.Subcommand('fields',
help='show fields available for queries and format strings')
def fields_func(lib, opts, args):
print("Available item fields:")
print("Item fields:")
print(" " + "\n ".join([key for key in library.ITEM_KEYS]))
print("\nAvailable album fields:")
print("\nAlbum fields:")
print(" " + "\n ".join([key for key in library.ALBUM_KEYS]))
plugin_fields = []
for plugin in plugins.find_plugins():
plugin_fields += plugin.template_fields.keys()
if plugin_fields:
print("\nTemplate fields from plugins:")
print(" " + "\n ".join(plugin_fields))
fields_cmd.func = fields_func
default_commands.append(fields_cmd)

View file

@ -468,14 +468,11 @@ def sanitize_path(path, pathmod=None, replacements=None):
reliably on Windows when a path begins with a drive letter. Path
separators (including altsep!) should already be cleaned from the
path components. If replacements is specified, it is used *instead*
of the default set of replacements for the platform; it must be a
list of (compiled regex, replacement string) pairs.
of the default set of replacements; it must be a list of (compiled
regex, replacement string) pairs.
"""
pathmod = pathmod or os.path
# Choose the appropriate replacements.
if not replacements:
replacements = list(CHAR_REPLACE)
replacements = replacements or CHAR_REPLACE
comps = components(path, pathmod)
if not comps:

View file

@ -677,3 +677,40 @@ class Configuration(RootView):
if not os.path.isdir(appdir):
os.makedirs(appdir)
return appdir
class LazyConfig(Configuration):
"""A Configuration at reads files on demand when it is first
accessed. This is appropriate for using as a global config object at
the module level.
"""
def __init__(self, appname, modname=None):
super(LazyConfig, self).__init__(appname, modname, False)
self._materialized = False # Have we read the files yet?
self._lazy_prefix = [] # Pre-materialization calls to set().
self._lazy_suffix = [] # Calls to add().
def read(self, user=True, defaults=True):
self._materialized = True
super(LazyConfig, self).read(user, defaults)
def resolve(self):
if not self._materialized:
# Read files and unspool buffers.
self.read()
self.sources += self._lazy_suffix
self.sources[:0] = self._lazy_prefix
return super(LazyConfig, self).resolve()
def add(self, value):
super(LazyConfig, self).add(value)
if not self._materialized:
# Buffer additions to end.
self._lazy_suffix += self.sources
del self.sources[:]
def set(self, value):
super(LazyConfig, self).set(value)
if not self._materialized:
# Buffer additions to beginning.
self._lazy_prefix[:0] = self.sources
del self.sources[:]

110
beetsplug/duplicates.py Normal file
View file

@ -0,0 +1,110 @@
# This file is part of beets.
# Copyright 2013, Pedro Silva.
#
# 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.
"""List duplicate tracks or albums.
"""
import logging
from beets.plugins import BeetsPlugin
from beets.ui import decargs, print_obj, Subcommand
PLUGIN = 'duplicates'
log = logging.getLogger('beets')
def _group_by_id(objs):
"""Return a dictionary whose keys are MBIDs and whose values are
lists of objects (Albums or Items) with that ID.
"""
import collections
counts = collections.defaultdict(list)
for obj in objs:
mbid = getattr(obj, 'mb_trackid', obj.mb_albumid)
counts[mbid].append(obj)
return counts
def _duplicates(objs, full):
"""Generate triples of MBIDs, duplicate counts, and constituent
objects.
"""
offset = 0 if full else 1
for mbid, objs in _group_by_id(objs).iteritems():
if len(objs) > 1:
yield (mbid, len(objs) - offset, objs[offset:])
class DuplicatesPlugin(BeetsPlugin):
"""List duplicate tracks or albums
"""
def __init__(self):
super(DuplicatesPlugin, self).__init__()
self.config.add({'format': ''})
self.config.add({'count': False})
self.config.add({'album': False})
self.config.add({'full': False})
self._command = Subcommand('duplicates',
help=__doc__,
aliases=['dup'])
self._command.parser.add_option('-f', '--format', dest='format',
action='store', type='string',
help='print with custom FORMAT',
metavar='FORMAT')
self._command.parser.add_option('-c', '--count', dest='count',
action='store_true',
help='count duplicate tracks or\
albums')
self._command.parser.add_option('-a', '--album', dest='album',
action='store_true',
help='show duplicate albums instead\
of tracks')
self._command.parser.add_option('-F', '--full', dest='full',
action='store_true',
help='show all versions of duplicate\
tracks or albums')
def commands(self):
def _dup(lib, opts, args):
self.config.set_args(opts)
fmt = self.config['format'].get()
count = self.config['count'].get()
album = self.config['album'].get()
full = self.config['full'].get()
if album:
items = lib.albums(decargs(args))
else:
items = lib.items(decargs(args))
# Default format string for count mode.
if count and not fmt:
if album:
fmt = '$albumartist - $album'
else:
fmt = '$albumartist - $album - $title'
fmt += ': {}'
for obj_id, obj_count, objs in _duplicates(items, full):
if obj_id: # Skip empty IDs.
for o in objs:
print_obj(o, lib, fmt=fmt.format(obj_count))
self._command.func = _dup
return [self._command]

View file

@ -18,6 +18,7 @@ import logging
import traceback
from beets.plugins import BeetsPlugin
from beets.library import Item, Album
from beets import config
log = logging.getLogger('beets')
@ -46,6 +47,14 @@ def _compile_func(body):
eval(code, env)
return env[FUNC_NAME]
def _record(obj):
"""Get a dictionary of values for an Item or Album object.
"""
if isinstance(obj, Item):
return dict(obj.record)
else:
return dict(obj._record)
def compile_inline(python_code):
"""Given a Python expression or function body, compile it as a path
field function. The returned function takes a single argument, an
@ -70,8 +79,8 @@ def compile_inline(python_code):
if is_expr:
# For expressions, just evaluate and return the result.
def _expr_func(item):
values = dict(item.record)
def _expr_func(obj):
values = _record(obj)
try:
return eval(code, values)
except Exception as exc:
@ -80,8 +89,8 @@ def compile_inline(python_code):
else:
# For function bodies, invoke the function with values as global
# variables.
def _func_func(item):
func.__globals__.update(item.record)
def _func_func(obj):
func.__globals__.update(_record(obj))
try:
return func()
except Exception as exc:

View file

@ -17,7 +17,7 @@
import logging
from beets.autotag import hooks
from beets.library import Item
from beets.library import Item, Album
from beets.plugins import BeetsPlugin
from beets.ui import decargs, print_obj, Subcommand
@ -135,16 +135,29 @@ class MissingPlugin(BeetsPlugin):
print(sum([_missing_count(a) for a in albums]))
return
# Default format string for count mode.
if count and not fmt:
fmt = '$albumartist - $album: $missing'
for album in albums:
if count:
missing = _missing_count(album)
if missing:
fmt = "$album: {}".format(missing)
print_obj(album, lib, fmt=fmt)
continue
for item in _missing(album):
print_obj(item, lib, fmt=fmt)
else:
for item in _missing(album):
print_obj(item, lib, fmt=fmt)
self._command.func = _miss
return [self._command]
@MissingPlugin.template_field('missing')
def _tmpl_missing(album):
"""Return number of missing items in 'album'.
"""
if isinstance(album, Album):
return _missing_count(album)
else:
return ''

View file

@ -19,6 +19,9 @@ from beets.plugins import BeetsPlugin
from beets.ui import Subcommand, decargs, print_obj
from beets.util.functemplate import Template
import random
from operator import attrgetter
from itertools import groupby
import collections
def random_item(lib, opts, args):
query = decargs(args)
@ -32,8 +35,33 @@ def random_item(lib, opts, args):
objs = list(lib.albums(query=query))
else:
objs = list(lib.items(query=query))
number = min(len(objs), opts.number)
objs = random.sample(objs, number)
if opts.equal_chance:
# Group the objects by artist so we can sample from them.
key = attrgetter('albumartist')
objs.sort(key=key)
objs_by_artists = {artist: list(v) for artist, v in groupby(objs, key)}
objs = []
for _ in range(opts.number):
# Terminate early if we're out of objects to select.
if not objs_by_artists:
break
# Choose an artist and an object for that artist, removing
# this choice from the pool.
artist = random.choice(objs_by_artists.keys())
objs_from_artist = objs_by_artists[artist]
i = random.randint(0, len(objs_from_artist) - 1)
objs.append(objs_from_artist.pop(i))
# Remove the artist if we've used up all of its objects.
if not objs_from_artist:
del objs_by_artists[artist]
else:
number = min(len(objs), opts.number)
objs = random.sample(objs, number)
for item in objs:
print_obj(item, lib, template)
@ -48,6 +76,8 @@ random_cmd.parser.add_option('-f', '--format', action='store',
help='print with custom format', default=None)
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',
help='each artist has the same chance')
random_cmd.func = random_item
class Random(BeetsPlugin):

View file

@ -4,8 +4,10 @@ Changelog
1.1.1 (in development)
----------------------
* New :doc:`/plugins/duplicates`: Find tracks or albums in your
library that are **duplicated**. Thanks to Pedro Silva.
* New :doc:`/plugins/missing`: Find albums in your library that are **missing
tracks**. Thanks to Pedro Silva.
tracks**. Thanks once more to Pedro Silva.
* New :doc:`/plugins/discogs`: Extends the autotagger to include matches from
the `discogs`_ database.
* Your library now keeps track of **when music was added** to it. The new
@ -16,6 +18,9 @@ Changelog
**numeric ranges**. For example, you can get a list of albums from the '90s
by typing ``beet ls year:1990..1999`` or find high-bitrate music with
``bitrate:128000..``. See :ref:`numericquery`. Thanks to Michael Schuerig.
* :doc:`/plugins/random`: A new ``-e`` option gives an equal chance to each
artist in your collection to avoid biasing random samples to prolific
artists. Thanks to Georges Dubus.
* The :ref:`modify-cmd` now correctly converts types when modifying non-string
fields. You can now safely modify the "comp" flag and the "year" field, for
example. Thanks to Lucas Duailibe.
@ -23,6 +28,11 @@ Changelog
Thanks to jayme on GitHub.
* :doc:`/plugins/lyrics`: Lyrics searches should now turn up more results due
to some fixes in dealing with special characters.
* Plugin-provided template fields now work for both Albums and Items. Thanks
to Pedro Silva.
* The :ref:`fields-cmd` command shows template fields provided by plugins.
Thanks again to Pedro Silva.
* Album art filenames now respect the :ref:`replace` configuration.
.. _discogs: http://discogs.com/

106
docs/guides/advanced.rst Normal file
View file

@ -0,0 +1,106 @@
Advanced Awesomeness
====================
So you have beets up and running and you've started :doc:`importing your
music </guides/tagger>`. There's a lot more that beets can do now that it has
cataloged your collection. Here's a few features to get you started.
Most of these tips involve :doc:`plugins </plugins/index>` and fiddling with
beets' :doc:`configuration </reference/config>`. So use your favorite text
editor create a config file before you continue.
Fetch album art, genres, and lyrics
-----------------------------------
Beets can help you fill in more than just the basic taxonomy metadata that
comes from MusicBrainz. Plugins can provide :doc:`album art
</plugins/fetchart>`, :doc:`lyrics </plugins/lyrics>`, and
:doc:`genres </plugins/lastgenre>` from databases around the Web.
If you want beets to get any of this data automatically during the import
process, just enable any of the three relevant plugins (see
:ref:`using-plugins`). For example, put this line in your :doc:`config file
</reference/config>` to enable all three::
plugins: fetchart lyrics lastgenre
Each plugin also has a command you can run to fetch data manually. For
example, if you want to get lyrics for all the Beatles tracks in your
collection, just type ``beet lyrics beatles`` after enabling the plugin.
Read more about using each of these plugins:
* :doc:`/plugins/fetchart` (and its accompanying :doc:`/plugins/embedart`)
* :doc:`/plugins/lyrics`
* :doc:`/plugins/lastgenre`
Customize your file and folder names
------------------------------------
Beets uses an extremely flexible template system to name the folders and files
that organize your music in your filesystem. Take a look at
:ref:`path-format-config` for the basics: use fields like ``$year`` and
``$title`` to build up a naming scheme. But if you need more flexibility,
there are two features you need to know about:
* :ref:`Template functions <template-functions>` are simple expressions you
can use in your path formats to add logic to your names. For example, you
can get an artist's first initial using ``%upper{%left{$albumartist,1}}``.
* If you need more flexibility, the :doc:`/plugins/inline` lets you write
snippets of Python code that generate parts of your filenames. The
equivalent code for getting an artist initial with the *inline* plugin looks
like ``initial: albumartist[0].upper()``.
If you already have music in your library and want to update their names
according to a new scheme, just run the :ref:`move-cmd` command to rename
everything.
Stream your music to another computer
-------------------------------------
Sometimes it can be really convenient to store your music on one machine and
play it on another. For example, I like to keep my music on a server at home
but play it at work (without copying my whole library locally). The
:doc:`/plugins/web` makes streaming your music easy---it's sort of like having
your own personal Spotify.
First, enable the ``web`` plugin (see :ref:`using-plugins`). Run the server by
typing ``beet web`` and head to http://localhost:8337 in a browser. You can
browse your collection with queries and, if your browser supports it, play
music using HTML5 audio.
But for a great listening experience, pair beets with the `Tomahawk`_ music
player. Tomahawk lets you listen to music from many different sources,
including a beets server. Just download Tomahawk and open its settings to
connect it to beets. `A post on the beets blog`_ has a more detailed guide.
.. _A post on the beets blog:
http://beets.radbox.org/blog/tomahawk-resolver.html
.. _Tomahawk: http://www.tomahawk-player.org
Transcode music files for media players
---------------------------------------
Do you ever find yourself transcoding high-quality rips to a lower-bitrate,
lossy format for your phone or music player? Beets can help with that.
You'll first need to install `ffmpeg`_. Then, enable beets'
:doc:`/plugins/convert`. Set a destination directory in your
:doc:`config file </reference/config>` like so::
convert:
dest: ~/converted_music
Then, use the command ``beet convert QUERY`` to transcode everything matching
the query and drop the resulting files in that directory, named according to
your path formats. For example, ``beet convert long winters`` will move over
everything by the Long Winters for listening on the go.
The plugin has many more dials you can fiddle with to get your conversions how
you like them. Check out :doc:`its documentation </plugins/convert>`.
.. _ffmpeg: http://www.ffmpeg.org

View file

@ -10,4 +10,5 @@ guide.
main
tagger
advanced
migration

View file

@ -152,9 +152,8 @@ metadata for every album you import. Option (a) is really fast, but option (b)
makes sure all your songs' tags are exactly right from the get-go. The point
about speed bears repeating: using the autotagger on a large library can take a
very long time, and it's an interactive process. So set aside a good chunk of
time if you're going to go that route. (I'm working on improving the
autotagger's performance and automation.) For more information on the
interactive tagging process, see :doc:`tagger`.
time if you're going to go that route. For more on the interactive
tagging process, see :doc:`tagger`.
If you've got time and want to tag all your music right once and for all, do
this::
@ -228,35 +227,14 @@ you have::
Artists: 548
Albums: 1094
Playing Music
-------------
Beets is primarily intended as a music organizer, not a player. It's designed to
be used in conjunction with other players (consider `Decibel`_ or `cmus`_;
there's even :ref:`a cmus plugin for beets <other-plugins>`). However, it does
include a simple music player---it doesn't have a ton of features, but it gets
the job done.
.. _Decibel: http://decibel.silent-blade.org/
.. _cmus: http://cmus.sourceforge.net/
The player, called BPD, is a clone of an excellent music player called `MPD`_.
Like MPD, it runs as a daemon (i.e., without a user interface). Another program,
called an MPD client, controls the player and provides the user with an
interface. You'll need to enable the BPD plugin before you can use it. Check out
:doc:`/plugins/bpd`.
.. _MPD: http://mpd.wikia.com/
You can, of course, use the bona fide MPD server with your beets library. MPD is
a great player and has more features than BPD. BPD just provides a convenient,
built-in player that integrates tightly with your beets database.
Keep Playing
------------
The :doc:`/reference/cli` page has more detailed description of all of beets'
functionality. (Like deleting music! That's important.) Start exploring!
This is only the beginning of your long and prosperous journey with beets. To
keep learning, take a look at :doc:`advanced` for a sampling of what else
is possible. You'll also want to glance over the :doc:`/reference/cli` page
for a more detailed description of all of beets' functionality. (Like
deleting music! That's important.)
Also, check out :ref:`included-plugins` as well as :ref:`other-plugins`. The
real power of beets is in its extensibility---with plugins, beets can do almost

105
docs/plugins/duplicates.rst Normal file
View file

@ -0,0 +1,105 @@
Duplicates Plugin
=================
This plugin adds a new command, ``duplicates`` or ``dup``, which finds
and lists duplicate tracks or albums in your collection.
Installation
------------
Enable the plugin by putting ``duplicates`` on your ``plugins`` line in
your :doc:`config file </reference/config>`::
plugins: duplicates
Configuration
-------------
By default, the ``beet duplicates`` command lists the names of tracks
in your library that are duplicates. It assumes that Musicbrainz track
and album ids are unique to each track or album. That is, it lists
every track or album with an ID that has been seen before in the
library.
You can customize the output format, count the number of duplicate
tracks or albums, and list all tracks that have duplicates or just the
duplicates themselves. These options can either be specified in the
config file::
duplicates:
format: $albumartist - $album - $title
count: no
album: no
full: no
or on the command-line::
-f FORMAT, --format=FORMAT
print with custom FORMAT
-c, --count count duplicate tracks or
albums
-a, --album show duplicate albums instead
of tracks
-F, --full show all versions of duplicate
tracks or albums
format
~~~~~~
The ``format`` option (default: :ref:`list_format_item`) lets you
specify a specific format with which to print every track or
album. This uses the same template syntax as beets :doc:`path formats
</reference/pathformat>`. The usage is inspired by, and therefore
similar to, the :ref:`list <list-cmd>` command.
count
~~~~~
The ``count`` option (default: false) prints a count of duplicate
tracks or albums, with ``format`` hard-coded to ``$albumartist -
$album - $title: $count`` or ``$albumartist - $album: $count`` (for
the ``-a`` option).
album
~~~~~
The ``album`` option (default: false) lists duplicate albums instead
of tracks.
full
~~~~
The ``full`` option (default: false) lists every track or album that
has duplicates, not just the duplicates themselves.
Examples
--------
List all duplicate tracks in your collection::
beet duplicates
List all duplicate tracks from 2008::
beet duplicates year:2008
Print out a unicode histogram of duplicate track years using `spark`_::
beet duplicates -f '$year' | spark
▆▁▆█▄▇▇▄▇▇▁█▇▆▇▂▄█▁██▂█▁▁██▁█▂▇▆▂▇█▇▇█▆▆▇█▇█▇▆██▂▇
Print out a listing of all albums with duplicate tracks, and respective counts::
beet duplicates -ac
The same as the above but include the original album, and show the path::
beet duplicates -acf '$path'
TODO
----
- Allow deleting duplicates.
.. _spark: https://github.com/holman/spark

View file

@ -5,6 +5,8 @@ Plugins can extend beets' core functionality. Plugins can add new commands to
the command-line interface, respond to events in beets, augment the autotagger,
or provide new path template functions.
.. _using-plugins:
Using Plugins
-------------
@ -62,8 +64,9 @@ disabled by default, but you can turn them on as described above.
smartplaylist
mbsync
missing
duplicates
discogs
Autotagger Extensions
''''''''''''''''''''''
@ -114,7 +117,8 @@ Miscellaneous
a different directory.
* :doc:`info`: Print music files' tags to the console.
* :doc:`missing`: List missing tracks.
* :doc:`duplicates`: List duplicate tracks or albums.
.. _MPD: http://mpd.wikia.com/
.. _MPD clients: http://mpd.wikia.com/wiki/Clients
@ -149,5 +153,5 @@ plugins </plugins/writing>`.
.. toctree::
:hidden:
writing

View file

@ -2,9 +2,10 @@ Missing Plugin
==============
This plugin adds a new command, ``missing`` or ``miss``, which finds
and lists, for every album in your collection, which tracks are
missing. Listing missing files requires one network call to
MusicBrainz.
and lists, for every album in your collection, which or how many
tracks are missing. Listing missing files requires one network call to
MusicBrainz. Merely counting missing files avoids any network calls.
Installation
------------
@ -49,8 +50,9 @@ inspired by, and therefore similar to, the :ref:`list <list-cmd>` command.
count
~~~~~
The ``count`` option (default: false) prints a count of missing
tracks per album, with ``format`` hard-coded to ``'$album: $count'``.
The ``count`` option (default: false) prints a count of missing tracks
per album, with ``format`` defaulting to ``$albumartist - $album:
$missing``.
total
~~~~~
@ -58,6 +60,11 @@ total
The ``total`` option (default: false) prints a single
count of missing tracks in all albums
Template Fields
---------------
With this plugin enabled, the ``$missing`` template field expands to the
number of tracks missing from each album.
Examples
--------
@ -83,6 +90,9 @@ Print out a count of the total number of missing tracks::
beet missing -t
Call this plugin from other beet commands::
beet ls -a -f '$albumartist - $album: $missing'
TODO
----

View file

@ -16,6 +16,10 @@ command (see :doc:`/reference/cli`). To choose an album instead of a single
track, use ``-a``; to print paths to items instead of metadata, use ``-p``; and
to use a custom format for printing, use ``-f FORMAT``.
If the ``-e`` option is passed, the random choice will be even among
artists (the albumartist field). This makes sure that your anthology
of Bob Dylan won't make you listen to Bob Dylan 50% of the time.
The ``-n NUMBER`` option controls the number of objects that are selected and
printed (default 1). To select 5 tracks from your library, type ``beet random
-n5``.

View file

@ -243,24 +243,28 @@ This plugin provides a function ``%initial`` to path templates where
``%initial{$artist}`` expands to the artist's initial (its capitalized first
character).
Plugins can also add template *fields*, which are computed values referenced as
``$name`` in templates. To add a new field, decorate a function taking a single
parameter, ``item``, with ``MyPlugin.template_field("name")``. Here's an example
that adds a ``$disc_and_track`` field::
Plugins can also add template *fields*, which are computed values referenced
as ``$name`` in templates. To add a new field, decorate a function taking a
single parameter, which may be an Item or an Album, with
``MyPlugin.template_field("name")``. Here's an example that adds a
``$disc_and_track`` field::
@MyPlugin.template_field('disc_and_track')
def _tmpl_disc_and_track(item):
def _tmpl_disc_and_track(obj):
"""Expand to the disc number and track number if this is a
multi-disc release. Otherwise, just exapnds to the track
number.
"""
if item.disctotal > 1:
return u'%02i.%02i' % (item.disc, item.track)
if isinstance(obj, beets.library.Album):
return u''
if obj.disctotal > 1:
return u'%02i.%02i' % (obj.disc, obj.track)
else:
return u'%02i' % (item.track)
return u'%02i' % (obj.track)
With this plugin enabled, templates can reference ``$disc_and_track`` as they
can any standard metadata field.
can any standard metadata field. Since the field is only meaningful for Items,
it expands to the empty string when used in an Album context.
Extend MediaFile
^^^^^^^^^^^^^^^^

View file

@ -199,6 +199,8 @@ overridden with ``-w`` (write tags, the default) and ``-W`` (don't write tags).
Finally, this command politely asks for your permission before making any
changes, but you can skip that prompt with the ``-y`` switch.
.. _move-cmd:
move
````
::
@ -246,6 +248,8 @@ Show some statistics on your entire library (if you don't provide a
The ``-e`` (``--exact``) option makes the calculation of total file size more
accurate but slower.
.. _fields-cmd:
fields
``````
::
@ -253,8 +257,7 @@ fields
beet fields
Show the item and album metadata fields available for use in :doc:`query` and
:doc:`pathformat`.
:doc:`pathformat`. Includes any template fields provided by plugins.
Global Flags
------------

View file

@ -43,6 +43,8 @@ probably don't want that! So use ``$albumartist``.
As a convenience, however, beets allows ``$albumartist`` to fall back to the value for ``$artist`` and vice-versa if one tag is present but the other is not.
.. _template-functions:
Functions
---------
@ -71,7 +73,7 @@ These functions are built in to beets:
* ``%aunique{identifiers,disambiguators}``: Provides a unique string to
disambiguate similar albums in the database. See :ref:`aunique`, below.
* ``%time{date_time,format}``: Return the date and time in any format accepted
by `strfime`_. For example, to get the year some music was added to your
by `strftime`_. For example, to get the year some music was added to your
library, use ``%time{$added,%Y}``.
.. _unidecode module: http://pypi.python.org/pypi/Unidecode

View file

@ -29,6 +29,7 @@ from _common import item
import beets.library
from beets import util
from beets import plugins
from beets import config
TEMP_LIB = os.path.join(_common.RSRC, 'test_copy.blb')
@ -835,10 +836,13 @@ class BaseAlbumTest(_common.TestCase):
class ArtDestinationTest(_common.TestCase):
def setUp(self):
super(ArtDestinationTest, self).setUp()
self.lib = beets.library.Library(':memory:')
config['art_filename'] = u'artimage'
config['replace'] = {u'X': u'Y'}
self.lib = beets.library.Library(
':memory:', replacements=[(re.compile(u'X'), u'Y')]
)
self.i = item()
self.i.path = self.lib.destination(self.i)
self.lib.art_filename = 'artimage'
self.ai = self.lib.add_album((self.i,))
def test_art_filename_respects_setting(self):
@ -850,6 +854,11 @@ class ArtDestinationTest(_common.TestCase):
track = self.lib.destination(self.i)
self.assertEqual(os.path.dirname(art), os.path.dirname(track))
def test_art_path_sanitized(self):
config['art_filename'] = u'artXimage'
art = self.ai.art_destination('something.jpg')
self.assert_('artYimage' in art)
class PathStringTest(_common.TestCase):
def setUp(self):
super(PathStringTest, self).setUp()

View file

@ -478,7 +478,7 @@ class ConfigTest(_common.TestCase):
if config_yaml:
config_data = yaml.load(config_yaml, Loader=confit.Loader)
config.set(config_data)
ui._raw_main(args + ['test'], False)
ui._raw_main(args + ['test'])
def test_paths_section_respected(self):
def func(lib, opts, args):