#!/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 . 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, 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): # 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='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") 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) @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())