beets/bts

276 lines
9.4 KiB
Python
Executable file

#!/usr/bin/env python
# This file is part of beets.
# Copyright 2009, Adrian Sampson.
#
# Beets is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Beets is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with beets. If not, see <http://www.gnu.org/licenses/>.
import cmdln
from ConfigParser import SafeConfigParser
import os
import sys
from beets import autotag
from beets import library
from beets import Library
from beets.mediafile import FileTypeError
CONFIG_DEFAULTS = {
# beets
'library': 'library.blb',
'directory': '~/Music',
'path_format': '$artist/$album/$track $title',
'import_copy': 'yes',
'import_write': 'yes',
# bpd
'host': '',
'port': '6600',
'password': '',
}
CONFIG_FILE = os.path.expanduser('~/.beetsrc')
def _print(txt):
"""Print the text encoded using UTF-8."""
print txt.encode('utf-8')
def _input_yn(prompt, require=False):
"""Prompts user for a "yes" or "no" response where an empty response
is treated as "yes". Keeps prompting until acceptable input is
given; returns a boolean. If require is True, then an empty response
is not accepted.
"""
resp = raw_input(prompt).strip()
while True:
if resp or not require:
if not resp or resp[0].lower() == 'y':
return True
elif len(resp) > 0 and resp[0].lower() == 'n':
return False
resp = raw_input("Type 'y' or 'n': ").strip()
def tag_album(items, lib, copy=True, write=True):
"""Import items into lib, tagging them as an album. If copy, then
items are copied into the destination directory. If write, then
new metadata is written back to the files' tags.
"""
# Infer tags.
try:
items,(cur_artist,cur_album),info,dist = autotag.tag_album(items)
except autotag.AutotagError:
print "Untaggable album:", os.path.dirname(items[0].path)
return
# Show what we're about to do.
if cur_artist != info['artist'] or cur_album != info['album']:
print "Correcting tags from:"
print ' %s - %s' % (cur_artist, cur_album)
print "To:"
print ' %s - %s' % (info['artist'], info['album'])
else:
print "Tagging: %s - %s" % (info['artist'], info['album'])
for item, track_data in zip(items, info['tracks']):
if item.title != track_data['title']:
print " * %s -> %s" % (item.title, track_data['title'])
# Warn if change is significant.
if dist > 0.0:
if not _input_yn("Apply change ([y]/n)? "):
return
# Ensure that we don't have the album already.
q = library.AndQuery((library.MatchQuery('artist', info['artist']),
library.MatchQuery('album', info['album'])))
count, _ = q.count(lib)
if count >= 1:
print "This album (%s - %s) is already in the library!" % \
(info['artist'], info['album'])
return
# Change metadata and add to library.
autotag.apply_metadata(items, info)
for item in items:
if copy:
item.move(lib, True)
lib.add(item)
if write:
item.write()
class BeetsApp(cmdln.Cmdln):
name = "bts"
def get_optparser(self):
# Add global options to the command.
parser = cmdln.Cmdln.get_optparser(self)
parser.add_option('-l', '--library', dest='libpath',
help='library database file to use')
parser.add_option('-d', '--directory', dest='directory',
help="destination music directory")
parser.add_option('-p', '--pathformat', dest='path_format',
help="destination path format string")
parser.add_option('-i', '--device', dest='device',
help="name of the device library to use")
return parser
def postoptparse(self):
# Read defaults from config file.
self.config = SafeConfigParser(CONFIG_DEFAULTS)
self.config.read(CONFIG_FILE)
for sec in ('beets', 'bpd'):
if not self.config.has_section(sec):
self.config.add_section(sec)
# Open library file.
if self.options.device:
from beets.device import PodLibrary
self.lib = PodLibrary.by_name(self.options.device)
else:
libpath = self.options.libpath or \
self.config.get('beets', 'library')
directory = self.options.directory or \
self.config.get('beets', 'directory')
path_format = self.options.path_format or \
self.config.get('beets', 'path_format')
self.lib = Library(os.path.expanduser(libpath),
directory,
path_format)
@cmdln.alias("imp", "im")
@cmdln.option('-c', '--copy', action='store_true', default=None,
help="copy tracks into library directory (default)")
@cmdln.option('-C', '--nocopy', action='store_false', dest='copy',
help="don't copy tracks (opposite of -c)")
@cmdln.option('-w', '--write', action='store_true', default=None,
help="write new metadata to files' tags (default)")
@cmdln.option('-W', '--nowrite', action='store_false', dest='write',
help="don't write metadata (opposite of -s)")
def do_import(self, subcmd, opts, *paths):
"""${cmd_name}: import new music
${cmd_usage}
${cmd_option_list}
"""
copy = opts.copy if opts.copy is not None else \
self.config.getboolean('beets', 'import_copy')
write = opts.write if opts.write is not None else \
self.config.getboolean('beets', 'import_write')
first = True
for path in paths:
for album in autotag.albums_in_dir(os.path.expanduser(path)):
if not first:
print
first = False
tag_album(album, self.lib, copy, write)
self.lib.save()
@cmdln.alias("ls")
@cmdln.option('-a', '--album', action='store_true',
help='show matching albums instead of tracks')
def do_list(self, subcmd, opts, *criteria):
"""${cmd_name}: query the library
${cmd_usage}
${cmd_option_list}
"""
q = ' '.join(criteria)
if not q.strip():
q = None # no criteria => match anything
if opts.album:
for artist, album in self.lib.albums(query=q):
_print(artist + ' - ' + album)
else:
for item in self.lib.items(query=q):
_print(item.artist + ' - ' + item.album + ' - ' + item.title)
@cmdln.alias("rm")
@cmdln.option("-d", "--delete", action="store_true",
help="also remove files from disk")
@cmdln.option('-a', '--album', action='store_true',
help='match albums instead of tracks')
def do_remove(self, subcmd, opts, *criteria):
"""${cmd_name}: remove matching items from the library
${cmd_usage}
${cmd_option_list}
"""
q = ' '.join(criteria).strip() or None
# Get the matching items.
if opts.album:
items = []
for artist, album in self.lib.albums(query=q):
items += list(self.lib.items(artist=artist, album=album))
else:
items = list(self.lib.items(query=q))
# Show all the items.
for item in items:
_print(item.artist + ' - ' + item.album + ' - ' + item.title)
# Confirm with user.
print
if opts.delete:
prompt = 'Really DELETE %i files (y/n)? ' % len(items)
else:
prompt = 'Really remove %i items from the library (y/n)? ' % \
len(items)
if not _input_yn(prompt, True):
return
# Remove and delete.
for item in items:
self.lib.remove(item)
if opts.delete:
os.unlink(item.path)
self.lib.save()
def do_bpd(self, subcmd, opts, host=None, port=None):
"""${cmd_name}: run an MPD-compatible music player server
${cmd_usage}
${cmd_option_list}
"""
host = host or self.config.get('bpd', 'host')
port = port or self.config.get('bpd', 'port')
password = self.config.get('bpd', 'password')
from beets.player.bpd import Server
Server(self.lib, host, int(port), password).run()
def do_dadd(self, subcmd, opts, name, *criteria):
"""${cmd_name}: add files to a device
${cmd_usage}
${cmd_option_list}
"""
q = ' '.join(criteria)
if not q.strip(): q = None
items = self.lib.items(query=q)
from beets import device
pod = device.PodLibrary.by_name(name)
for item in items:
pod.add(item)
pod.save()
if __name__ == '__main__':
app = BeetsApp()
sys.exit(app.main())