#!/usr/bin/env python
################################################################################
#
#  Copyright (C) 2002-2004  Travis Shirk <travis@pobox.com>
#
#  This program 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 2 of the License, or
#  (at your option) any later version.
#
#  This program 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 this program; if not, write to the Free Software
#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
################################################################################
import os, sys, math, optik;
try:
    from optparse import *;
except:
    # Python 2.2
    from optik import *;
from stat import *;
try:
   import eyeD3, eyeD3.utils;
   from eyeD3 import *;
except ImportError:
   # For development
   sys.path.append("../src");
   import eyeD3, eyeD3.utils;


class ConsoleColors(dict):
   use = 1;

   def __init__(self):
      self["normal"]  = chr(0x1b) + "[0m";
      self["header"]  = chr(0x1b) + "[32m";
      self["warning"] = chr(0x1b) + "[33m";
      self["error"]   = chr(0x1b) + "[31m";
      self["bold"]    = chr(0x1b) + "[1m";

   def enabled(self, b):
      self.use = b;

   # Accessor override.
   def __getitem__(self, key):
      if self.use:
         return dict.__getitem__(self, key);
      else:
         return "";
colors = ConsoleColors();


################################################################################
class OptionParserHelpFormatter(HelpFormatter):

    def __init__(self, indent_increment=2, max_help_position=24, width=80,
                 short_first=1):
        HelpFormatter.__init__ (self, indent_increment, max_help_position,
                                width, short_first);

    def format_option_strings_short_first (self, option):
        opts = [];                       # list of "-a" or "--foo=FILE" strings
        takes_value = option.takes_value();
        if takes_value:
            metavar = option.metavar or option.dest.upper();
            for sopt in option._short_opts:
                opts.append(sopt + " " + metavar);
            for lopt in option._long_opts:
                opts.append(lopt + "=" + metavar);
        else:
            for opt in option._short_opts + option._long_opts:
                opts.append(opt);
        return ", ".join(opts);

    def format_option_strings_long_first (self, option):
        opts = [];      # list of "-a" or "--foo=FILE" strings
        takes_value = option.takes_value();
        if takes_value:
            metavar = option.metavar or option.dest.upper();
            for lopt in option._long_opts:
                opts.append(lopt + "=" + metavar);
            for sopt in option._short_opts:
                opts.append(sopt + " " + metavar);
        else:
            for opt in option._long_opts + option._short_opts:
                opts.append(opt);
        return ", ".join(opts);

    def format_usage(self, usage):
        return "\n%s  %s\n" % (self.format_heading("Usage"), usage);

    def format_heading(self, heading):
        return "%s\n%s\n" % (heading, "=-"[self.level] * len(heading));
        #return "%*s%s:\n" % (self.current_indent, "", heading)


################################################################################
def getOptionParser():
   versionStr =\
"%prog " + eyeD3.eyeD3Version + " (C) Copyright 2003 " +\
eyeD3.eyeD3Maintainer + "\n" + \
"This program comes with ABSOLUTELY NO WARRANTY! See COPYING for details.\n";

   usageStr = "%prog [OPTS] file [file...]";
   helpFormatter = OptionParserHelpFormatter();
   optParser = OptionParser(usage=usageStr, version=versionStr,
                            formatter=helpFormatter);
   optParser.disable_interspersed_args();

   # Version options.
   versOpts = OptionGroup(optParser, "Tag Versions");
   versOpts.add_option("-1", "--v1", action="store_const",
                       const=eyeD3.ID3_V1, dest="tagVersion",
                       default=eyeD3.ID3_ANY_VERSION,
                       help="Only read/write ID3 v1.x tags. By default, "\
                             "v1.x tags are only read if there is not a v2.x "\
                             "tag.");
   versOpts.add_option("-2", "--v2", action="store_const",
                       const=eyeD3.ID3_V2, dest="tagVersion",
                       default=eyeD3.ID3_ANY_VERSION,
                       help="Only read/write ID3 v2.x tags.");
   versOpts.add_option("--to-v1.1", action="store_const",
                       const=eyeD3.ID3_V1_1, dest="convertVersion",
                       default=0,
                       help="Convert the file's tag to ID3 v1.1."\
                            " (Or 1.0 if there is no track number.)");
   versOpts.add_option("--to-v2.3", action="store_const",
                       const=eyeD3.ID3_V2_3, dest="convertVersion",
                       default=0,
                       help="Convert the file's tag to ID3 v2.3");
   versOpts.add_option("--to-v2.4", action="store_const",
                       const=eyeD3.ID3_V2_4, dest="convertVersion",
                       default=0,
                       help="Convert the file's tag to ID3 v2.4");
   optParser.add_option_group(versOpts);

   # Tag data options.
   grp1 = OptionGroup(optParser, "Tag Data");
   grp1.add_option("-a", "--artist", action="store", type="string",
                   dest="artist", metavar="STRING",
                   help="Set artist");
   grp1.add_option("-A", "--album", action="store", type="string",
                   dest="album", metavar="STRING",
                   help="Set album");
   grp1.add_option("-t", "--title", action="store", type="string",
                   dest="title", metavar="STRING",
                   help="Set title");
   grp1.add_option("-n", "--track", action="store", type="string",
                   dest="track", metavar="NUM",
                   help="Set track number");
   grp1.add_option("-N", "--track-total", action="store", type="string",
                   dest="track_total", metavar="NUM",
                   help="Set total number of tracks");
   grp1.add_option("-G", "--genre", action="store", type="string",
                   dest="genre", metavar="GENRE",
                   help="Set genre. The argument is a valid genre string or "\
                        "number.  See --list-genres");
   grp1.add_option("-Y", "--year", action="store", type="string",
                   dest="year", metavar="STRING",
                   help="Set a four digit year.");
   grp1.add_option("--comment", action="append", type="string",
                   dest="comments", metavar="[LANGUAGE]:[DESCRIPTION]:COMMENT",
                   help="Set comment");
   grp1.add_option("--remove-comments", action="store_true", 
                   dest="remove_comments", help="Remove all comment frames.");
   grp1.add_option("--add-image", action="append", type="string",
                   dest="images", metavar="IMG_PATH:TYPE[:DESCRIPTION]",
                   help="Add an image to the tag.  The description and type "\
                        "optional, but when used, both ':' delimiters must "\
                        "be present.  The type MUST be an string that "
                        "corresponds to one given with --list-image-types.");
   grp1.add_option("--list-image-types", action="store_true",
                   dest="showImagesTypes",
                   help="List all possible image types"); 
   grp1.add_option("-i", "--write-images", action="store_true",
                   dest="writeImages",
                   help="Causes all attached images (APIC frames) to be "\
                        "written to the current directory.");
   grp1.add_option("--set-text-frame", action="append", type="string",
                   dest="textFrames", metavar="FID:TEXT", 
                   help="Set the value of a text frame.  To remove the "\
                        "frame, specify an empty value.  "\
                        "e.g., --set-text-frame=\"TDRC:\"");
   grp1.add_option("--remove-v1", action="store_true",
                    dest="remove_v1", default=0,
                    help="Remove ID3 v1.x tag.");
   grp1.add_option("--remove-v2", action="store_true",
                    dest="remove_v2", default=0,
                    help="Remove ID3 v2.x tag.");
   grp1.add_option("--remove-all", action="store_true",
                    dest="remove_all", default=0,
                    help="Remove both ID3 v1.x and v2.x tags.");
   optParser.add_option_group(grp1);

   # Misc. options.
   grp3 = OptionGroup(optParser, "Misc. Options");
   grp3.add_option("-l", "--list-genres", action="store_true",
                   dest="showGenres",
                   help="Display the table of ID3 genres and exit");
   grp3.add_option("--strict", action="store_true",
                   dest="strict", help="Fail for tags that violate "\
                                       "the ID3 specification.");
   grp3.add_option("--jep-118", action="store_true", dest="jep_118",
                   help="Output the tag per the format described in JEP-0118. "\
                        "See http://www.jabber.org/jeps/jep-0118.html");
   grp3.add_option("--no-color", action="store_true",
                   dest="nocolor", help="Disable color output");
   grp3.add_option("-v", "--verbose", action="store_true",
                   dest="verbose", help="Show all available information");
   grp3.add_option("--debug", action="store_true", dest="debug",
                   help="Trace program execution.");
   optParser.add_option_group(grp3);

   return optParser;

################################################################################
def printGenres():
   cols = 2;
   offset = int(math.ceil(float(len(eyeD3.genres)) / cols));
   for i in range(offset):
      if i < len(eyeD3.genres):
         c1 = "%3d: %s" % (i, eyeD3.genres[i]);
      else:
         c1 = "";
      if (i * 2) < len(eyeD3.genres):
         c2 = "%3d: %s" % (i + offset, eyeD3.genres[i + offset]);
      else:
         c2 = "";
      print c1 + (" " * (40 - len(c1))) + c2;

################################################################################
def printImageTypes():
    print "Available image types for --add-image:";
    iframe = eyeD3.frames.ImageFrame();
    for type in range(iframe.MIN_TYPE, iframe.MAX_TYPE + 1):
        print "  o " + iframe.picTypeToString(type);

################################################################################
def boldText(s, c = None):
   if c:
      return colors["bold"] + c + s + colors["normal"];
   return colors["bold"] + s + colors["normal"];

def printWarning(s):
   print colors["warning"] + str(s) + colors["normal"];

def printError(s):
   print colors["error"] + str(s) + colors["normal"];

################################################################################
class EyeD3Driver:
   opts = None;
   files = [];

   def __init__(self, opts):
      self.opts = opts;

   def run(self, files):
      self.files = files;

      for f in files:
         audioFile = None;
         tag = None;

         try:
            if eyeD3.isMp3File(f):
               audioFile = eyeD3.Mp3AudioFile(f, self.opts.tagVersion);
               tag = audioFile.getTag();
            else:
               tag = eyeD3.Tag();
               if not tag.link(f, self.opts.tagVersion):
                  tag = None;
         except (eyeD3.InvalidAudioFormatException,
                 eyeD3.TagException, IOError), ex:
            printError(ex);
            continue;

         if tag and self.opts.jep_118:
             print tagToUserTune(audioFile);
             return 0;

         self.printHeader(f);
         self.printAudioInfo(audioFile);

         if not tag:
            printError("No ID3 %s tag found!" %\
                       eyeD3.utils.versionToString(self.opts.tagVersion));

         try:
            # Handle frame removals.
            if tag and self.handleRemoves(tag):
               tag = None;

            # Create a a new tag in case values are being added, or the tag
            # was removed.
            newTag = 0;
            if not tag:
               tag = eyeD3.Tag(f);
               tag.header.setVersion(self.opts.tagVersion);
               newTag = 1;

            # Handle frame edits.
            tagModified = self.handleEdits(tag);
            if newTag and not tagModified and not self.opts.convertVersion:
               continue;

            # Handle updating the tag if requested.
            if tagModified or self.opts.convertVersion:
               updateVersion = eyeD3.ID3_CURRENT_VERSION;
               if self.opts.convertVersion:
                  updateVersion = self.opts.convertVersion;
                  v = eyeD3.utils.versionToString(updateVersion);
                  if updateVersion == tag.getVersion():
                     printWarning("No conversion necessary, tag is "\
                                  "already version %s" % v);
                  else:
                     printWarning("Converting tag to ID3 version %s" % v);
               elif self.opts.tagVersion != eyeD3.ID3_ANY_VERSION:
                  updateVersion = self.opts.tagVersion;

               # Update the tag.
               printWarning("Writing tag...");
               if not tag.update(updateVersion):
                  printError("Error writing tag: %s" % f);
                  return 1;
            else:
               if newTag:
                  # No edits were performed so we can ditch the _new_ tag.
                  tag = None;
         except eyeD3.TagException, ex:
            printError(ex);
            continue;

         # Print tag.
         try:
             if tag:
                self.printTag(tag);
                if self.opts.verbose:
                   print "-" * 80;
                   print "ID3 Frames:";
                   for frm in tag:
                      print frm;
         except (UnicodeEncodeError, UnicodeDecodeError), ex:
            printError(ex);
            continue;
      return 0; 

   def handleRemoves(self, tag):
      # Remove if requested.
      removeVersion = 0;
      status = 0;
      rmStr = "";
      if self.opts.remove_all:
         removeVersion = eyeD3.ID3_ANY_VERSION;
         rmStr = "v1.x and/or v2.x";
      elif self.opts.remove_v1:
         removeVersion = eyeD3.ID3_V1;
         rmStr = "v1.x";
      elif self.opts.remove_v2:
         removeVersion = eyeD3.ID3_V2;
         rmStr = "v2.x";

      if removeVersion:
         status = tag.remove(removeVersion);
         statusStr = self.boolToStatus(status);
         printWarning("Removing ID3 %s tag: %s" % (rmStr, status));

      return status;

   def handleEdits(self, tag):
      retval = 0;
      artist = self.opts.artist;
      album = self.opts.album;
      title = self.opts.title;
      trackNum = self.opts.track;
      trackTotal = self.opts.track_total;
      genre = self.opts.genre;
      year = self.opts.year;
      comments = self.opts.comments;

      if artist != None:
         printWarning("Setting artist: %s" % artist);
         tag.setArtist(artist);
         retval |= 1;

      if album != None:
         printWarning("Setting album: %s" % album);
         tag.setAlbum(album);
         retval |= 1;

      if title != None:
         printWarning("Setting title: %s" % title);
         tag.setTitle(title);
         retval |= 1;

      if trackNum != None or trackTotal != None:
         if trackNum:
            printWarning("Setting track: %s" % str(trackNum));
            trackNum = int(trackNum);
         else:
            trackNum = tag.getTrackNum()[0];
         if trackTotal:
            printWarning("Setting track total: %s" % str(trackTotal));
            trackTotal = int(trackTotal);
         else:
            trackTotal = tag.getTrackNum()[1];
         tag.setTrackNum((trackNum, trackTotal));
         retval |= 1;

      if genre != None:
         printWarning("Setting track genre: %s" % genre);
         tag.setGenre(genre);
         retval |= 1;

      if year != None:
         printWarning("Setting year: %s" % year);
         tag.setDate(year);
         retval |= 1;

      if self.opts.remove_comments:
         count = tag.removeComments();
         printWarning("Removing %d comment frames" % count);
         retval |= 1;
      elif comments:
         for c in comments:
            try:
               (lang,desc,comm) = c.split(":");
               if not lang:
                  lang = eyeD3.DEFAULT_LANG;
               printWarning("Setting comment: %s" % comm);
               tag.addComment(comm, desc, lang);
               retval |= 1;
            except ValueError:
               printError("Invalid Comment; see --help: %s" % c);
               retval &= 0;

      if self.opts.images:
          for i in self.opts.images:
              img_args = i.split(":");
              if len(img_args) < 2:
                  raise TagException("Invalid --add-image argument: %s" % i);
              else:
                 type = eyeD3.frames.ImageFrame().stringToPicType(img_args[0]);
                 path = img_args[1];
                 printWarning("Adding image %s" % path);
                 desc = u"";
                 if (len(img_args) > 2) and img_args[2]:
                     desc = unicode(img_args[2]);
                 tag.addImage(type, path, desc);
                 retval |= 1;

      if self.opts.textFrames:
          for tf in self.opts.textFrames:
              tf_args = tf.split(":");
              if len(tf_args) < 2:
                  raise TagException("Invalid --set-text-frame argument: "\
                                     "%s" % tf);
              else:
                  if tf_args[1]:
                      printWarning("Setting %s frame to '%s'" % (tf_args[0],
                                                                 tf_args[1]));
                  else:
                      printWarning("Removing %s frame" % (tf_args[0]));
                  try:
                      tag.setTextFrame(tf_args[0], tf_args[1]);
                      retval |= 1;
                  except FrameException, ex:
                      printError(ex);
                      retval &= 0;

      return retval;

   def boolToStatus(self, b):
      if b:
         return "SUCCESS";
      else:
         return "FAIL";

   def printHeader(self, filePath):
      fileSize = float(os.stat(filePath)[ST_SIZE]) / 1048576.0;
      print "";
      print "%s\t%s[ %.2f MB ]%s" % (boldText(os.path.basename(filePath),
                                              colors["header"]),
                                     colors["header"], fileSize,
                                     colors["normal"]);
      print ("-" * 80);

   def printAudioInfo(self, audioInfo):
      if isinstance(audioInfo, eyeD3.Mp3AudioFile):
         print boldText("Time: ") +\
               "%s\tMPEG%d, Layer %s\t[ %s @ %s Hz - %s ]" %\
               (audioInfo.getPlayTimeString(), audioInfo.header.version,
                "I" * audioInfo.header.layer, audioInfo.getBitRateString(),
                audioInfo.header.sampleFreq, audioInfo.header.mode);
         print ("-" * 80);
      else:
         # Handle what it is known and silently ignore anything else.
         pass;

   def printTag(self, tag):
      if isinstance(tag, eyeD3.Tag):
        
         print "ID3 %s:" % tag.getVersionStr();
         print "%s: %s\t\t%s: %s" % (boldText("title"), tag.getTitle(),
                                     boldText("artist"), tag.getArtist());
         
         print "%s: %s\t\t%s: %s" % (boldText("album"), tag.getAlbum(),
                                     boldText("year"), tag.getYear());

         trackStr = "";
         (trackNum, trackTotal) = tag.getTrackNum();
         if trackNum != None:
            trackStr = str(trackNum);
            if trackTotal:
               trackStr += "/%d" % trackTotal;
         genre = None;
         genreStr = "";
         try:
            genre = tag.getGenre();
         except eyeD3.GenreException, ex:
            printError(ex);
         if genre:
            genreStr = "%s (id %d)" % (genre.getName(), genre.getId());
         print "%s: %s\t\t%s: %s" % (boldText("track"), trackStr,
                                     boldText("genre"), genreStr);
         
         comments = tag.getComments();
         if comments:
            print "";
         for c in comments:
            cLang = c.lang;
            if cLang == None:
               cLang = "";
            cDesc = c.description;
            if cDesc == None:
               cDesc = "";
            cText = c.comment;
            print "%s: [Description: %s] [Lang: %s]\n%s" % \
               (boldText("Comment"), cDesc, cLang, cText);

         userTextFrames = tag.getUserTextFrames();
         if userTextFrames:
            print "";
         for f in userTextFrames:
            desc = f.description;
            if not desc:
               desc = "";
            text = f.text;
            print "%s: [Description: %s]\n%s" % \
                  (boldText("UserTextFrame"), desc, text);
 
         urls = tag.getURLs();
         if urls:
            print "";
         for u in urls:
            if u.header.id != eyeD3.frames.USERURL_FID:
               print "%s: %s" % (u.header.id, u.url);
            else:
               print "%s [Description: %s]:\n%s" % (u.header.id, u.description,
                                                    u.url);
 
         images = tag.getImages();
         if images:
            print "";
         for img in images:
            print "%s: [Size: %d bytes] [Type: %s]" % \
                  (boldText(img.picTypeToString(img.pictureType) + " Image"),
                   len(img.imageData), img.mimeType);
            print "Description: %s" % img.description;
            print "\n";
            if self.opts.writeImages:
               print "Writing %s..." % img.getDefaultFileName();
               img.writeFile();
      else:
         raise TypeError("Unknown tag type: " + str(type(tag)));

################################################################################
def dirVisit(fileList, dirname, files):
   for f in files:
      if eyeD3.isMp3File(f) or os.path.splitext(f)[1] == ".tag":
         fileList.append(os.path.join(dirname, f));

################################################################################
def main():

   # Process command line.
   optParser = getOptionParser();
   (options, args) = optParser.parse_args();
   # Handle -l, --list-genres
   if options.showGenres:
      printGenres();
      return 0;
   # Handle --list-image-types
   if options.showImagesTypes:
      printImageTypes();
      return 0;
   if len(args) == 0:
      optParser.error("File/directory argument(s) required");
      return 1;
   # Handle -d, --debug
   if options.debug:
      eyeD3.utils.TRACE = 1;
   # Handle --strict
   if options.strict:
      eyeD3.utils.STRICT_ID3 = 1;
   # Handle --nocolor
   if options.nocolor:
      colors.enabled(0);

   # Obtain list of files.
   files = [];
   for a in args:
      if os.path.isfile(a):
         files.append(a);
      elif os.path.isdir(a):
         os.path.walk(a, dirVisit, files);
      else:
         printError("File Not Found: %s" % a);
         

   app = EyeD3Driver(options);
   return app.run(files);

#######################################################################
if __name__ == "__main__":
    try:
        retval = main();
    except KeyboardInterrupt:
        retval = 0;
    sys.exit(retval);
