#!/usr/bin/env python
# coding=utf-8
"""
Music tagging and management tool
"""

import os
import sys
import re
import shutil
import argparse

from datetime import datetime, timedelta

from musa.sync import SyncManager, SyncError
from musa.transcoder import MusaTranscoder, TranscoderError

from soundforest import normalized, SoundforestError
from musa.cli import MusaScript, MusaScriptCommand, ScriptError
from soundforest.tree import Tree, Album, Track, TreeError
from soundforest.tags import TagError
from soundforest.formats import match_metadata
from soundforest.metadata import CoverArt
from soundforest.playlist import m3uPlaylist, m3uPlaylistDirectory, PlaylistError
from soundforest.tags.xmltag import XMLTrackTree, XMLTagError
from soundforest.tags.albumart import AlbumArt, AlbumArtError

# Tags not to copy in copytags
IGNORED_COPY_TAGS = (
    'replaygain_reference_loudness'
)

class AlbumArtCommand(MusaScriptCommand):

    """AlbumArtCommand

    Embed or extract albumart objects in supported audio files

    """
    def show_info(self, target):
        if isinstance(target, Track):
            tags = self.get_tags(target)
            if tags is None or not tags.supports_albumart:
                return
            try:
                albumart = tags.albumart
            except AlbumArtError, emsg:
                return
            self.message('%s: %s' % (target.path, albumart))

        elif isinstance(target, Tree):
            for album in target.as_albums():
                album.load()
                for m in album.metadata:
                    if hasattr(m, 'metadata') and isinstance(m.metadata, CoverArt):
                        self.print_coverart_info(m)
                    else:
                        self.message(m)

    def print_coverart_info(self, coverart):
        albumart = AlbumArt(coverart.path)
        self.message('%s: %s' % (coverart.path, albumart))

    def embed(self, target, albumart=None):
        if isinstance(target, Tree):
            albums = target.as_albums()

            if not albums:
                self.script.exit(1, 'No albums found from path %s' % target.path)
            if len(albums) > 1 and albumart is not None:
                self.script.exit(1, 'Given albumart can only embedded to single album tree targets')

            album = albums[0]

            if albumart is None:
                albumart = album.albumart
            if albumart is None:
                self.script.exit('No albumart found from album %s' % album.path)
                return

            for track in album:
                tags = track.tags
                if tags is None or not tags.supports_albumart:
                    self.log.debug('albumart not supported: %s' % track.path)
                    continue
                self.log.debug('embed: %s' % track.path)
                tags.set_albumart(albumart)
                tags.save()

        elif isinstance(target, Track):
            if albumart is None:
                track_album = Album(os.path.dirname(target.path))
                albumart = track_album.albumart
            if albumart is None:
                return
            tags = target.tags
            if tags is None or not tags.supports_albumart:
                self.log.debug('albumart not supported: %s' % target.path)
                return
            self.log.debug('embed: %s' % target.path)
            tags.set_albumart(albumart)
            tags.save()

    def extract(self, target, overwrite=True):
        if isinstance(target, Tree):
            albums = target.as_albums()
            if not albums:
                self.script.exit(1, 'No albums found from path %s' % target.path)

            for album in albums:
                track = album[0]
                tags = track.tags

                if tags is None:
                    self.script.error('No tags in %s' % track.path)
                    continue

                if not tags.albumart:
                    self.script.error('No album art in %s' % track.path)
                    continue

                albumart_path = os.path.join(os.path.dirname(track.path), 'artwork.jpg')

                if not overwrite and os.path.isfile(albumart_path):
                    self.log.debug('Skip existing: %s' % albumart_path)

                else:
                    try:
                        tags.albumart.save(albumart_path)
                        self.log.debug('Saving %s: %s' % (albumart_path, tags.albumart))
                    except TagError, emsg:
                        self.log.debug('Error saving %s: %s' % (albumart_path, emsg))

        elif isinstance(target, Track):
            tags = target.tags

            if tags is None:
                self.script.error('No tags in %s' % target.path)
                return

            if not tags.albumart:
                self.script.error('No album art in %s' % target.path)
                return

            albumart_path = os.path.join(os.path.dirname(target.path), 'artwork.jpg')

            if not overwrite and os.path.isfile(albumart_path):
                self.log.debug('Skip existing: %s' % albumart_path)

            else:
                try:
                    tags.albumart.save(albumart_path)
                    self.log.debug('Saving %s: %s' % (albumart_path, tags.albumart))
                except TagError, emsg:
                    self.log.debug('Error saving %s: %s' % (albumart_path, emsg))

    def run(self, args):
        trees, tracks, metadata = MusaScriptCommand.run(self, args)

        if args.action == 'info':
            for m in metadata:
                if not isinstance(m, CoverArt):
                    continue
                self.print_coverart_info(m)

            for tree in trees:
                self.show_info(tree)

            for track in tracks:
                self.show_info(track)

        if args.action == 'embed':
            if args.url:
                albumart = AlbumArt()

                try:
                    albumart.fetch(args.url)
                except AlbumArtError, emsg:
                    self.script.exit(1, 'Error fetching albumart: %s' % emsg)

                for target in trees:
                    albumart.save(target.path)

            else:
                albumart = None

            for tree in trees:
                self.embed(tree, albumart)

            for track in tracks:
                self.embed(track, albumart)

        if args.action in ('extract', 'extract-missing'):
            overwrite = args.action == 'extract'

            for tree in trees:
                self.extract(tree, overwrite=overwrite)

            for track in tracks:
                self.extract(track, overwrite=overwrite)


class CleanupCommand(MusaScriptCommand):
    def cleanup_tree(self, tree, dry_run):
        if not isinstance(tree, Tree):
            self.script.exit(1, 'BUG: cleanup_tree argument not Tree instance')

        for path in tree.invalid_paths:
            metadata = match_metadata(path)
            if metadata:
                if not metadata.removable:
                    continue
                self.log.debug('Unwanted metadata %s: %s' % (metadata, path))
            else:
                self.log.debug('Unknown file type: %s' % path)
            try:
                os.unlink(path)
            except OSError, (ecode, emsg):
                self.log.message('Error removing %s: %s' % (path, emsg))

        # Reload tree to get empty trees
        self.log.debug('Reloading tree to process empty directories')
        tree.load()
        for empty in tree.empty_dirs:
            tree.remove_empty_path(empty)

    def run(self, args):
        trees, tracks, metadata = MusaScriptCommand.run(self, args)

        if not len(trees):
            self.script.exit(1, 'Cleanup command only valid for trees')

        for tree in trees:
            self.cleanup_tree(tree, dry_run=args.dry_run)


class CodecCommand(MusaScriptCommand):
    def run(self, args):
        trees, tracks, metadata = MusaScriptCommand.run(self, args)

        if args.action == 'register-tester':
            if len(args.names) != 1:
                script.error(1,'Only one valid codec name can be provided with --register-tester command')

            try:
                codec = self.script.db.codecs[args.names[0]]
                codec.register_formattester(self.script.db.session,args.register_tester)
            except MusaError,emsg:
                script.exit(1,emsg)

        if args.action == 'unregister-tester':
            if len(args.names) != 1:
                script.error(1,'Only one valid codec name can be provided with --unregister-tester command')

            try:
                codec = self.script.db.codecs[args.names[0]]
                codec.unregister_formattester(self.script.db.session,args.unregister_tester)
            except MusaError,emsg:
                script.exit(1,emsg)

        if args.action == 'list':
            for name, codec in self.script.db.codecs.items():
                if args.names and name not in args.names:
                    continue
                self.message('%s' % name)
                self.message('  Description: %s' % codec.description)
                self.message('  Extensions:  %s' % ','.join(e.extension for e in codec.extensions))

                if codec.encoders:
                    self.message('  Encoders:')
                    for encoder in codec.encoders:
                        script.message('    %s' % encoder)

                if codec.decoders:
                    self.message('  Decoders:')
                    for decoder in codec.decoders:
                        script.message('    %s' % decoder)

                if codec.formattesters:
                    self.message('   Testers:')
                    for tester in codec.formattesters:
                        script.message('    %s' % tester)


class CompareCommand(MusaScriptCommand):
    def compare_trees(self, src, dst):
        src_track_paths = [x.relative_path.no_ext for x in src]
        dst_track_paths = [x.relative_path.no_ext for x in dst]
        if src_track_paths != dst_track_paths:
            script.message('Filename mismatch')
            for s in src_tracks:
                script.message(type(s.relative_path), s.relative_path.no_ext)
        for track in src:
            if track.relative_path.no_ext not in dst_track_paths:
                script.message('%s: missing %s' % (dst.relative_path, track.relative_path.no_ext))
                continue

    def compare_tracks(self, src, dst):
        if isinstance(src, basestring):
            try:
                src = Track(src)
            except TreeError, emsg:
                self.script.exit(1, emsg)
        if isinstance(src, basestring):
            try:
                dst = Track(dst)
            except TreeError, emsg:
                self.script.exit(1, emsg)
        self.log.debug('Comparing %s to %s' % (src.relative_path, dst.relative_path))
        src_tags = src.tags
        dst_tags = dst.tags

        for tag in sorted(src_tags.keys()):
            if tag not in dst_tags.keys():
                self.script.error('%s: missing %s' % (dst.relative_path, tag))

        for tag in sorted(dst_tags.keys()):
            if tag not in src_tags.keys():
                self.script.error('%s: missing %s' % (src.relative_path, tag))

        for tag in sorted(src_tags.keys()):
            if tag not in dst_tags.keys():
                continue
            if src_tags[tag] != dst_tags[tag]:
                self.script.error('tag %s: %s != %s' % (tag, src_tags[tag], dst_tags[tag]))

    def run(self, args):
        trees, tracks, metadata = MusaScriptCommand.run(self, args)

        if trees and tracks:
            self.script.exit(1, "Can't compare trees to tracks")

        if trees and len(trees) == 2:
            self.compare_trees(*trees)
        elif tracks and len(tracks) == 2:
            self.compare_tracks(*tracks)
        elif metadata:
            self.script.exit(1, 'One of tracks was a metadata file')
        else:
            self.script.exit(1, 'Requires two directory or file arguments')



class ConfigCommand(MusaScriptCommand):
    def run(self, args):
        MusaScriptCommand.run(self, args, skip_targets=True)

        if args.action == 'set':
            if args.key is None or args.value is None:
                self.script.exit(1, 'Both key and value are required')
            self.script.db.set(args.key, args.value)

        if args.action == 'get':
            print self.script.db.get(args.key)

        if args.action == 'list':
            for k, v in self.script.db.items():
                self.message('%s=%s' % (k, v))


class ConvertCommand(MusaScriptCommand):
    def enqueue(self, src, dst):
        """
        Enqueue tracks to trascode and mark track metadata for copying
        """
        if not isinstance(src, Track) or not isinstance(dst, Track):
            self.script.exit(1, 'BUG: enqueue parameters must be Track objects')

        src_album = Album(os.path.dirname(src.path))
        dst_album = Album(os.path.dirname(dst.path))

        if src_album.path not in self.metadata_paths_to_copy.keys():
            self.metadata_paths_to_copy[src_album.path] = (src_album, dst_album)

        if os.path.isfile(dst.path) and not self.overwrite:
            self.log.debug('File exists: %s' % dst.path)
            return

        try:
            self.transcoder.enqueue(src, dst)
        except TranscoderError, emsg:
            self.script.exit(1, emsg)

    def parse_target_relative_path(self,src,dst=None,prefix_path=None,codec=None):
        if not isinstance(src, Track):
            self.script.exit(1, 'BUG in parse_target_relative_path: src must be Track object')

        if codec is not None:
            if prefix_path is not None:
                prefix = self.prefixes.match_extension(codec, match_existing=True)
                if prefix is None:
                    if isinstance(codec, basestring):
                        extensions = [codec]

                    elif hasattr(codec, 'extensions'):
                        extensions = codec.extensions

                    else:
                        self.script.exit(1, 'Unsupported codec value: %s' % codec)

                    prefix = self.prefixes.register_prefix(prefix_path, extensions)
            else:
                prefix = self.prefixes.match_extension(codec, match_existing=True)

        elif dst is not None:
            prefix = self.prefixes.match(dst.path, match_existing=True)
            if prefix is None:
                prefix = self.prefixes.match(dst.path, match_existing=False)

        else:
            raise ScriptError(
                'BUG in parse_target_relative_path: Both dst and codec were None'
            )

        if prefix is not None:
            extension = prefix.extensions[0]
            if prefix_path is None:
                prefix_path = prefix.path
        else:
            extension = codec

        if prefix_path is None and src.relative_path.startswith(os.sep):
            # Could not parse relative path for source: encode to src directory
            prefix_path = os.path.dirname(os.path.realpath(src.path))
            dst_relpath = '%s.%s' % (os.path.splitext(src.path)[0], extension)
        else:
            # Encode to relative path in prefix directory
            dst_relpath = '%s.%s' % (os.path.splitext(src.relative_path)[0], extension)

        return Track(os.path.join(prefix_path, dst_relpath))

    def transcode(self, src, dst=None, prefix_path=None, output_file=None, codec=None):
        if output_file:
            if not isinstance(src, Track):
                self.script.exit(1, 'Unsupported transcode arguments')
            self.enqueue(src, Track(os.path.realpath(output_file)))
            return

        try:
            if isinstance(src, Tree) and isinstance(dst, Tree):
                for track in src:
                    dst_track = self.parse_target_relative_path(
                        track, dst, prefix_path, codec
                    )
                    self.enqueue(track, dst_track)

            elif isinstance(src, Track) and (isinstance(dst, Tree) or isinstance(dst, Track)):
                dst_track = self.parse_target_relative_path(src, dst, prefix_path, codec)
                self.enqueue(src, dst_track)

            elif isinstance(src, Track) and dst is None:
                dst_track = self.parse_target_relative_path(
                    src,
                    prefix_path=prefix_path,
                    codec=codec
                )
                self.enqueue(src, dst_track)

            else:
                self.script.exit(1, 'Unsupported transcode arguments')
        except ScriptError, emsg:
            self.script.exit(1, emsg)

    def copy_metadata(self, dry_run=False):
        for src_path in sorted(self.metadata_paths_to_copy.keys()):
            src_album, dst_album = self.metadata_paths_to_copy[src_path]
            self.log.debug('metadata: %s' % dst_album.path)
            src_album.copy_metadata(dst_album)

    def run(self, args):
        trees, tracks, metadata = MusaScriptCommand.run(self, args)

        track_count = sum(len(d.files) for d in trees) + len(tracks)
        if not track_count:
            self.script.exit(1, 'No music files detected')

        threads = args.threads is not None and args.threads or self.script.db.get('threads')

        self.transcoder = MusaTranscoder(threads, args.overwrite, args.dry_run)
        self.overwrite = args.overwrite

        self.metadata_paths_to_copy = {}
        if args.output and track_count==1:
            try:
                self.transcode(tracks[0], output_file=args.output)
            except IndexError:
                self.script.exit(1, 'Converting a single file requires one source track path')
        elif args.codecs is not None:
            for codec in args.codecs.split(','):
                for tree in trees:
                    for track in tree:
                        self.transcode(track, prefix_path=args.prefix, codec=codec)
                for track in tracks:
                    self.transcode(track, prefix_path=args.prefix, codec=codec)

        elif len(trees) == 2:
            self.transcode(*trees, prefix_path=args.prefix)

        elif len(tracks) == 2:
            self.transcode(*tracks, prefix_path=args.prefix)

        if len(self.transcoder):
            self.transcoder.run()
        else:
            self.script.error('Nothing to transcode')

        if args.metadata:
            self.copy_metadata(args.dry_run)


class CopyTagsCommand(MusaScriptCommand):
    re_original = re.compile('^[0-9-]+\s+(?P<name>.*)$')

    def clone_tags(self, src, dst):
        """
        Clone tags from src to dst
        """
        modified = False
        for tag, value in src.tags.items():
            if tag in IGNORED_COPY_TAGS:
                continue

            try:
                target_value = dst.tags.get_tag(tag)
            except TagError, emsg:
                target_value = None
                pass

            if value == target_value:
                continue

            if not modified:
                print 'Updating: %s' % dst.path

            print '  UPDATE %s: %s -> %s' % (tag, target_value, value)
            dst.tags[tag] = value
            modified = True

        if modified:
            dst.tags.save()
        return modified

    def run(self, args):
        MusaScriptCommand.run(self, args, skip_targets=True)

        source = Tree(args.source)
        if args.target_extension:
            target_extension = args.target_extension
        else:
            target_prefix = self.prefixes.match(args.target, match_existing=True)
            if not target_prefix:
                script.exit(1, 'Error looking up target tree %s' % args.target)
            target_extension = target_prefix.extensions[0]

        processed = 0
        updated = 0
        started = datetime.now()
        source_parts = source.path.rstrip(os.sep).split(os.sep)
        for source_track in source:
            processed += 1
            if args.progress_interval is not None and  processed%args.progress_interval == 0:
                script.message('Processed: %d tracks' % processed)

            track_parts = source_track.path.split(os.sep)
            tp = os.path.join(args.target, os.sep.join(track_parts[len(source_parts):]))
            try:
                target_track = Track('%s.%s' % (os.path.splitext(tp)[0], target_extension))
            except TreeError:
                continue
            if not os.path.isfile(target_track.path):
                continue

            if args.mtime_compare and target_track.mtime >= source_track.mtime:
                continue

            if self.clone_tags(source_track, target_track):
                updated += 1

        ended = datetime.now()
        script.log.debug('Processed %d tracks, updated %d tracks in %s seconds' % (
            processed, updated, (ended-started).total_seconds()
        ))


class DatabaseCommand(MusaScriptCommand):
    def run(self, args):
        MusaScriptCommand.run(self, args, skip_targets=True)

        if args.action == 'register':
            for dbt in args.trees:
                try:
                    self.script.db.register_tree(dbt)
                except MusaError, emsg:
                    self.script.exit(1, emsg)

        elif args.action == 'unregister':
            for dbt in args.trees:
                self.script.db.unregister_tree(dbt)
            script.exit(0)

        if args.trees:
            trees = []
            for dbt in args.trees:
                entry = self.script.db.get_tree(dbt)
                if entry is None:
                    self.script.exit(1, 'Path not registered: %s' % dbt)
                trees.append(entry)
        else:
            trees = self.script.db.trees

        if args.action == 'list':
            for dbt in self.script.db.trees:
                print 'Tree: %s' % dbt.path

        if args.action == 'list-tracks':
            for dbt in self.script.db.trees:
                print 'Tree: %s' % dbt.path
                for album in dbt.albums:
                    if album.relative_path:
                        print '  %s' % album.relative_path
                    for track in album.tracks:
                        print '    %s' % track.filename
                        for tag in track.tags:
                            print '      %s=%s' % (tag.tag, tag.value)

        if args.action == 'update':
            for dbt in trees:
                self.script.log.debug('Updating database entries for %s' % dbt.path)
                dbt.update(self.script.db.session, Tree(dbt.path))

        elif args.action == 'match':
            for dbt in trees:
                tracks = dbt.match(self.script.db.session, args.match)
                if len(tracks):
                    self.log.info('### %d matches in %s ###' % (len(tracks, ), dbt))
                for track in tracks:
                    self.script.message(track.relative_path)
                    if args.info:
                        for t in track.tags:
                            self.script.message('%16s %s' % (t.tag, t.value))

        elif args.action == 'info':
            for dbt in trees:
                script.message(dbt)
                script.message('Albums: %s' % dbt.album_count(self.script.db.session))
                script.message('Songs: %s' % dbt.song_count(self.script.db.session))
                script.message('Tags: %s' % dbt.tag_count(self.script.db.session))


class JoinCommand(MusaScriptCommand):
    re_original = re.compile('^[0-9-]+\s+(?P<name>.*)$')

    def new_track_name(self, target_path, index, track):
        """Return new track name

        Return new track name to use for given track in target directory

        """
        original = os.path.basename(track.path)
        m = self.re_original.match(original)
        if not m:
            script.exit(1, 'Track does not begin with track number: %s' % track.path)
        return os.path.join(target_path, '%02d %s' % (index, m.groupdict()['name']))

    def run(self, args):
        MusaScriptCommand.run(self, args, skip_targets=True)

        if not args.paths:
            script.exit(1, 'Paths to join not provided')

        if not args.target_path:
            args.target_path = args.paths[0]

        albums = []
        extensions = []
        for path in args.paths:
            if not os.path.isdir(path):
                script.exit(1, 'Not a directory: %s' % path)
            try:
                album = Album(path)
            except MusaError, emsg:
                script.exit(1, 'Error parsing album %s: %s' % (path, emsg))

            for track in album:
                if track.extension not in extensions:
                    extensions.append(track.extension)

            albums.append(album)

        if len(extensions) > 1:
            script.exit(1, 'Refusing to merge: multiple track extensions detected: %s' % ','.join(extensions))

        index = 1
        tracks = []
        for album in albums:
            for track in album:
                tracks.append((track.path, self.new_track_name(args.target_path, index, track)))
                index += 1

        if len(tracks) == 0:
            script.exit(1, 'No audio tracks detected.')

        if args.dry_run:
            for track in tracks:
                print track[1]
            script.exit()

        for entry in tracks:
            if entry[0] == entry[1]:
                continue

            try:
                os.rename(entry[0], entry[1])
            except OSError, (ecode, emsg):
                script.exit(1, 'Error renaming %s: %s' % (entry[0], emsg))

class PlaylistCommand(MusaScriptCommand):

    def run(self, args):
        MusaScriptCommand.run(self, args, skip_targets=True)

        if args.action == 'register':
            for path in args.paths:
                try:
                    self.script.db.register_playlist_source(path)
                except MusaError, emsg:
                    script.exit(1, emsg)
            script.exit(0)

        elif args.action == 'unregister':
            for path in args.paths:
                try:
                    self.script.db.unregister_playlist_source(path)
                except MusaError, emsg:
                    script.exit(1, emsg)
            script.exit(0)

        if args.paths:
            sources = []
            for path in args.paths:
                entry = self.script.db.get_playlist_source(path)
                if entry is None:
                    self.script.exit(1, 'Path not registered: %s' % path)
                sources.append(entry)
        else:
            sources = self.script.db.playlist_sources

        if args.action == 'update':
            for source in sources:
                m3u_directory = m3uPlaylistDirectory(source.path)
                script.log.debug('Updating source: %s' % source.path)
                source.update(self.script.db.session, m3u_directory)

        if args.action == 'list':
            for source in sources:
                script.log.debug('Playlist source: %s' % source.path)
                for playlist in source.playlists:
                    print playlist
                    for path in playlist.tracks:
                        print '  %s' % path


class SyncCommand(MusaScriptCommand):

    def run(self, args):
        MusaScriptCommand.run(self, args, skip_targets=True)
        self.manager = SyncManager(threads=args.threads, delete=args.delete, debug=args.debug)

        if args.list:
            for name, settings in self.script.db.sync.items():
                if args.paths and name not in args.paths:
                    continue
                self.message('%s' % name)
                self.message('  Type:        %s' % settings['type'])
                self.message('  Source:      %s' % settings['src'])
                self.message('  Destination: %s' % settings['dst'])
                self.message('  Flags:       %s' % settings['flags'])
            script.exit(0)

        if args.directories:
            if len([d for d in args.paths if os.path.isdir(d)]) != 2:
                self.script.exit(1, 'Directory sync requires two existing directory paths')
            src = Tree(args.paths[0])
            dst = Tree(args.paths[1])
            self.manager.enqueue({
                'type': 'directory',
                'src':src,
                'dst':dst,
                'rename':args.rename
            })

        elif args.paths:
            for arg in args.paths:
                target = self.manager.parse_target(arg)
                if not target:
                    self.script.exit(1, 'No such target: %s' % arg)
                self.manager.enqueue(target)
        else:
            for target in self.manager.db.sync.default_targets:
                self.manager.enqueue(self.manager.parse_target(target))

        if len(self.manager):
            self.manager.run()
        else:
            self.script.exit(1, 'No sync targets found')


class TagsCommand(MusaScriptCommand):
    def __init__(self, *args, **kwargs):
        MusaScriptCommand.__init__(self, *args, **kwargs)
        self.xmltree = XMLTrackTree()

    def print_tags(self, track, print_path=False, path_format=None,
                   raw_tags=False, xml=False, json=False, show_tags=[]):

        path_format = path_format is not None and path_format or '# %s'

        tags = self.get_tags(track)
        if tags is None:
            return

        if xml:
            self.xmltree.append(tags.as_xml())

        elif json:
            self.message(tags.to_json())

        elif raw_tags:
            if print_path:
                self.message(path_format % track.path)
            for k, v in tags.get_raw_tags():
                if show_tags and k not in show_tags:
                    continue
                self.message('%s %s' % (k, v))
        else:
            if print_path:
                self.message(path_format % track.path)
            for tag, values in tags.items():
                if show_tags and k not in show_tags:
                    continue

                for v in values:
                    self.message('%s %s' % (tag, v))

    def edit_tags(self, track):
        """
        Edit tags with external editor command
        """
        track_tags = self.get_tags(track)
        if track_tags is None:
            return None

        new_tags = self.script.edit_tags(track_tags.as_dict())
        if new_tags != track_tags.as_dict():
            try:
                if track_tags.replace_tags(new_tags):
                    track_tags.save()
                    db_track = self.script.db.get_track(track.path)
                    if db_track is not None:
                        db_track.update(self.script.db.session, track)
            except TagError, emsg:
                self.script.exit(1, 'Error saving tags to %s: %s' % (track.path, emsg))

    def remove_tags(self, track, tags):
        track_tags = self.get_tags(track)
        if track_tags is None:
            return None
        try:
            if track_tags.remove_tags(tags):
                track_tags.save()
                db_track = self.script.db.get_track(track.path)
                if db_track is not None:
                    db_track.update(self.script.db.session, track)

        except TagError, emsg:
            self.script.exit(1, 'Error removing tags from %s: %s' % (track.path, emsg))

    def clear_tags(self, track):
        """
        Clear all tags from track
        """
        track_tags = self.get_tags(track)
        if track_tags is None:
            return None
        try:
            track_tags.clear_tags()
            db_track = self.script.db.get_track(track.path)
            if db_track is not None:
                db_track.update(self.script.db.session, track)

        except TagError, emsg:
            self.script.exit(1, 'Error removing tags from %s: %s' % (track.path, emsg))

    def tags_from_path(self, track, tags={}):
        parts = normalized(track.relative_path).split(os.sep)
        if len(parts) >= 3:
            tags['album_artist'] = parts[-3]
            tags['artist'] = parts[-3]
            tags['album'] = parts[-2]
            tracknumber, title = track.tracknumber_and_title
            tags['title'] = title
            if tracknumber is not None:
                tags['tracknumber'] = tracknumber
                tags['totaltracks'] = len(track.album)
        self.update_tags(track, tags)

    def update_tags(self, track, tags):
        track_tags = self.get_tags(track)
        if track_tags is None:
            return None

        try:
            if track_tags.update_tags(tags):
                track_tags.save()
                db_track = self.script.db.get_track(track.path)
                if db_track is not None:
                    db_track.update(self.script.db.session, track)

        except TagError, emsg:
            self.script.exit(1, 'Error saving tags to %s: %s' % (track.path, emsg))

    def run(self, args):
        trees, tracks, metadata = MusaScriptCommand.run(self, args)

        if args.xml and args.json:
            self.script.exit(1, 'Flags --xml and --json are mutually exclusive')

        if args.path_format and not args.print_path:
            # If path format is given, set print_path on as well
            args.print_path = True

        track_count = sum(len(d.files) for d in trees) + len(tracks)
        if not track_count:
            self.script.exit(1, 'No music files detected')

        if args.clear:
            self.process_tracks(trees, tracks, self.clear_tags)

        if args.from_path:
            self.process_tracks(trees, tracks, self.tags_from_path)

        if args.delete:
            tags = args.delete
            self.process_tracks(trees, tracks, self.remove_tags, tags=tags)

        if args.set:
            try:
                tags = {}
                for tag in args.set:
                    key, value = tag.split('=', 1)
                    if key not in tags:
                        tags[key] = []
                    tags[key].append(value)
            except ValueError:
                self.script.exit('Invalid arguments to --set flag: %s' % args.set)
            self.process_tracks(trees, tracks, self.update_tags, tags=tags)

        if args.input_file:
            try:
                tags = self.read_input_to_dict(args.input_file)
            except ScriptError, emsg:
                self.script.exit(1, str(emsg).strip())
            self.process_tracks(trees, tracks, self.update_tags, tags=tags)

        if args.edit:
            self.process_tracks(trees, tracks, self.edit_tags)

        # Finally, allow listing tags even if we were editing them earlier
        if args.list or not self.selected_mode_flags:
            self.process_tracks(
                trees,
                tracks,
                self.print_tags,
                print_path=args.print_path,
                path_format=args.path_format,
                raw_tags=args.print_raw,
                xml=args.xml,
                json=args.json,
                show_tags=args.get,
            )

            if args.xml:
                self.message(self.xmltree.tostring())

script = MusaScript()
c = script.add_subcommand(AlbumArtCommand('albumart', 'Manage music file album art'))
c.add_argument('-u', '--url', help='Fetch artwork from given url')
c.add_argument('action', choices=('embed', 'extract', 'extract-missing', 'info'), help='Action to perform')
c.add_argument('paths', metavar='path', nargs='*', help='Paths to process')

c = script.add_subcommand(CleanupCommand('cleanup', 'Remove unwanted metadata files from tree'))
c.add_argument('-y', '--dry-run', action='store_true', help='Only show files to be removed')
c.add_argument('paths', metavar='path', nargs='*', help='Paths to process')

c = script.add_subcommand(CodecCommand('codecs', 'Configure codec command stored to database'))
c.add_argument('action', choices=('list', 'register-tester', 'unregister-tester', ), help='Action to run')
c.add_argument('names', nargs='*', help='Codec names to match')

c = script.add_subcommand(CompareCommand('compare', 'Compare music files'))
c.add_argument('paths', metavar='path', nargs='*', help='Paths to process')

c = script.add_subcommand(ConfigCommand('config', 'Configure musa settings'))
c.add_argument('action', choices=('list', 'set', 'get') )
c.add_argument('key', nargs='?', help='Configuration key')
c.add_argument('value', nargs='?', help='Configuration value')

c = script.add_subcommand(ConvertCommand('convert', 'Transcode audio file formats'))
c.add_argument('-t', '--threads', type=int, default=1, help='Number of transcoder threads to use')
c.add_argument('-m', '--metadata', action='store_true', help='Copy album metadata')
c.add_argument('-y', '--dry-run', action='store_true', help='Only show which tracks would have been transcoded')
c.add_argument('-f', '--overwrite', action='store_true', help='Overwrite existing target files')
c.add_argument('-o', '--output', help='Specify output filename for single file conversion')
c.add_argument('-p', '--prefix', help='Target file relative path prefix')
c.add_argument('-c', '--codecs', help='Destination codecs for tree mode')
c.add_argument('paths', metavar='path', nargs='*', help='Paths to process')

c = script.add_subcommand(CopyTagsCommand('copytags', 'Copy tags from source tree to target tree'))
c.add_argument('source', help='Source tree')
c.add_argument('target', help='Target tree')
c.add_argument('--progress-interval', type=int, help='How often report number of processed tracks')
c.add_argument('--target-extension', help='Target tree filename extension')
c.add_argument('--mtime-compare', action='store_true', help='Use mtime to compare tracks')

c = script.add_subcommand(DatabaseCommand('db', description = 'Manage music file database'))
c.add_argument('action', choices=('list', 'list-tracks', 'match', 'info', 'update', 'register', 'unregister'))
c.add_argument('trees', nargs='*', help='Tree paths to process')

c = script.add_subcommand(JoinCommand('join', 'Join albums to one directory'))
c.add_argument('--target-path', help='Target path for files (default first path)')
c.add_argument('-y', '--dry-run', action='store_true', help='List new track names without renaming')
c.add_argument('paths', nargs='*', help='Directories to join')

c = script.add_subcommand(PlaylistCommand('playlist', 'Manipulate playlists'))
c.add_argument('action', choices=('list', 'update', 'register', 'unregister'))
c.add_argument('paths', nargs='*', help='Tree paths to process')

c = script.add_subcommand(SyncCommand('sync', 'Synchronize files and trees'))
c.add_argument('-d', '--directories', action='store_true', help='Sync directories, not configured targets')
c.add_argument('-l', '--list', action='store_true', help='List configured sync targets')
c.add_argument('-r', '--rename', help='Directory sync target filesystem rename callback')
c.add_argument('-D', '--delete', action='store_true', help='Remove unknown files from target')
c.add_argument('-t', '--threads', type=int, help='Number of sync threads to use')
c.add_argument('paths', metavar='path', nargs='*', help='Paths to process')

c = script.add_subcommand(TagsCommand('tags', 'Manage music file tags',
    mode_flags = ['set', 'clear', 'delete', 'edit', 'from_path', 'input_file']
))
c.add_argument('-l', '--list', action='store_true', help='List tags in given files')
c.add_argument('-g', '--get', action='append', help='Get listed tags')
c.add_argument('-s', '--set', action='append', help='Set tag from value (tag=value)')
c.add_argument('-i', '--input-file', type=argparse.FileType('r'), help='Set new tags from input file')
c.add_argument('-e', '--edit', action='store_true', help='Edit tags in external editor')
c.add_argument('-f', '--from-path', action='store_true', help='Guess tags to set from file path')
c.add_argument('-d', '--delete', action='append', help='Delete tag')
c.add_argument('-C', '--clear', action='store_true', help='Clear all tags')
c.add_argument('-p', '--print-path', action='store_true', help='Print file path before tags')
c.add_argument('-r', '--print-raw', action='store_true', help='Print raw tag values')
c.add_argument('-x', '--xml', action='store_true', help='XML output')
c.add_argument('-j', '--json', action='store_true', help='JSON output')
c.add_argument('-P', '--path-format', help='String format for --print-path flag')
c.add_argument('paths', metavar='path', nargs='*', help='Paths to process')

script.run()
