mirror of
https://github.com/beetbox/beets.git
synced 2026-01-07 08:32:06 +01:00
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:
commit
75a4171d20
24 changed files with 534 additions and 99 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -20,4 +20,4 @@ from beets.util import confit
|
|||
|
||||
Library = beets.library.Library
|
||||
|
||||
config = confit.Configuration('beets', __name__, False)
|
||||
config = confit.LazyConfig('beets', __name__)
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
110
beetsplug/duplicates.py
Normal 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]
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 ''
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
106
docs/guides/advanced.rst
Normal 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
|
||||
|
|
@ -10,4 +10,5 @@ guide.
|
|||
|
||||
main
|
||||
tagger
|
||||
advanced
|
||||
migration
|
||||
|
|
|
|||
|
|
@ -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
105
docs/plugins/duplicates.rst
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
----
|
||||
|
|
|
|||
|
|
@ -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``.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
|
|
|||
|
|
@ -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
|
||||
------------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in a new issue