beets/bts
2010-04-06 17:49:19 -07:00

202 lines
6.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',
# 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):
"""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.
"""
resp = raw_input(prompt)
while True:
if len(resp) == 0 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': ")
def tag_album(items, lib):
# 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:
item.move(lib, True)
lib.add(item)
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='the library database file to use')
parser.add_option('-d', '--device', dest='device',
help="the 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.config.get('beets', 'directory')
path_format = self.config.get('beets', 'path_format')
self.lib = Library(os.path.expanduser(libpath),
directory,
path_format)
@cmdln.alias("imp", "im")
def do_import(self, subcmd, opts, *paths):
"""${cmd_name}: import new music
${cmd_usage}
${cmd_option_list}
"""
for path in paths:
for album in autotag.albums_in_dir(os.path.expanduser(path)):
print
tag_album(album, self.lib)
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)
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())