Source

beets / bts

#!/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')
        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.
        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())