#! /usr/bin/perl -w

#--------------------------------------------------------------------------
# Query or set MP3 ID3v1 or Ogg Vorbis tag information
#--------------------------------------------------------------------------
 
use strict;
use File::Basename;
use File::Copy;
use File::Spec;
use File::Temp qw/tempfile/;
use FileHandle;
use Getopt::Long;
use MP3::Tag;
use FindBin qw($RealBin);
use lib "$RealBin/lib";
use SongInfo::MP3;
use SongInfo::Song;

# Debug level. 0: 0ff, 1: show args, 2: show args and then exit
use constant DEBUG => 0;


my %opts = (
    tag => [],
);
my (@inPaths, %tags, $cleanUp);
my $basename = basename $0;
my $usage = <<USAGE;
$basename v1.2.1 - Query or set MP3 ID3 or Ogg Vorbis tag information

usage:
  $basename [-2lb] INFILE
  $basename -a|m|w -t "key1=value 1" [-t "key2=value 2"] [-o OUTFILE ] INFILES
  $basename -a|m|w -c commentfile [-o OUTFILE ] INFILES
  $basename -e [-o OUTFILE ] INFILES
  $basename -k [-o OUTFILE ] INFILES
  $basename -L [-o OUTFILE ] INFILES
  $basename -g
  $basename -h | --help

  -2, --id3v2             List the ID3v2 tags
  -a, --append            Append (new, if possible, like Ogg) comments
  -b, --both              List both ID3v1 and ID3v2 tags
  -c, --commentfile=FILE  Take comments from a file. The file is the same 
                          format as is output by the -l option: one element 
                          per line in 'tag=value' format
  -e, --edit              Edit tags in-place with a text editor
  -g, --genre             List MP3 ID3v1 genres in 3-column format
  -h, --help              This help information
  -k, --kill              Kill ID3v2 tags altogether
  -l, --list              List the tags, default: true
  -L, --lowercase         Change all tag names to lowercase. This is really
                          only meaningful for Ogg Vorbis
  -m, --modify            Modify existing comments with ones given using the
                          -t or -c switches
  -o, --out=FILE          Don't modify original file, write to FILE instead
  -t, --tag=TAG=VALUE     Tag name and value, can use multiple -t switches.
                          Specify a new tag on the command line. Each tag is 
                          given as a single string. The part before the '=' 
                          is treated as the tag name and the part after as 
                          the value
  -w, --write             Replace all comments with the new set given either 
                          on the command line with -t or from a file with -c

  Valid tag keys:
  ID3v1: album artist comment date genre title tracknumber
  Ogg Vorbis: album artist cddb comment contact copyright date description 
    encoder genre isrc license location organization performer sortstring 
    title tracknumber version
    (With Ogg, keys can be repeated, how cool is that?)

  This program is meant to work almost exactly like vorbiscomment. Some of
  the command help was borrowed nearly word-for-word from it.
USAGE


sub showTags {
	my $song = new SongInfo::Song(shift);

	my $fh = shift;
	$fh ||= FileHandle->new('>-');

	my @keys = sort $song->getKeys();
	for my $key (@keys) {
		my @values = $song->getTag($key);
		for my $value (@values) {
			print $fh "$key=$value$/";
		}
	}

	$song->close();

    print $fh "\n" if (@inPaths > 1);
}


# Show ID3v2 tags for the file
sub showV2Tags {
	my $mp3 = new MP3::Tag(shift);
	$mp3->get_tags();

	if (exists $mp3->{ID3v2}) {
		my $id3v2 = $mp3->{ID3v2};
		my $frameIDs_hash = $id3v2->get_frame_ids('truename');

		foreach my $frame (keys %$frameIDs_hash) {
			my ($name, @info) = $id3v2->get_frame($frame);
			for my $info (@info) {
				# It's one of the frames with children
				if (ref $name) {
					print "$frame\n";
					while(my ($key,$val)=each %$name)
					{ print "  $key: $val\n"; }
				}
				# It's one of the simple key/value type frames
				else { print "$info: $name\n"; }
			}
		}  # foreach
	}  # if

	$mp3->close();

    print "\n" if (@inPaths > 1);
}


# Remove ID3v2 tags.
sub killV2Tags {
	my ($inPath, $outPath) = @_;

	copy("$inPath", "$outPath") or die "Copy failed: $!";

	my $mp3 = new MP3::Tag("$outPath");
	$mp3->get_tags();

	if (exists $mp3->{ID3v2}) { $mp3->{ID3v2}->remove_tag(); }

	$mp3->close();
}


sub editTags {
	my ($inPath, $outPath) = @_;

	copy("$inPath", "$outPath") or die "Copy failed: $!";
	my $song = new SongInfo::Song($outPath);

	# Arg w or e means get rid of all old tags
	if(($opts{write}) || ($opts{edit})) { $song->clear(); }

	# Lay the new tags in there.
	my $tagsChanged;
	foreach (keys %tags) {
		$tagsChanged = 1;
        if ($opts{modify}) {
            $song->editTag($_, $tags{$_});
        } else {
            $song->addTag($_, $tags{$_});
        }
	}

	$song->write() if($tagsChanged);

	$song->close();
}


# Change existing tags to lowercase
sub lcTags {
	my ($inPath, $outPath) = @_;

	copy("$inPath", "$outPath") or die "Copy failed: $!";
	my $song = new SongInfo::Song($outPath);

    # Extract the tags, storing them with lowercase key names
    my %tags;
	for my $key ($song->getKeys()) {
		my @values = $song->getTag($key);
		for my $value (@values) {
            $tags{lc $key} = $value;
		}
	}

	# Get rid of all old tags
    $song->clear();

	# Lay the new tags in there.
	foreach (keys %tags) {
        $song->addTag($_, $tags{$_});
	}

	$song->write();

	$song->close();
}


sub loadCommentFile {
	my $commentPath = shift;

	open (COMMENT_FILE, "$commentPath") || die "Can't open $commentPath\n";
	while (<COMMENT_FILE>) {
		# Get rid of the trailing newline.
		chop;

		/(.*?)=(.*)/;
		$tags{$1} = $2;
	}
	close COMMENT_FILE;
}


sub beforeNoOutfile {
    my $rInPath = shift;

	unless ($opts{out}) {
		$opts{out} = $$rInPath;
		$$rInPath .= ".mutag-tmp";
		move($opts{out}, $$rInPath);
		$cleanUp = 1;
	}
}


sub afterNoOutfile {
    my $rInPath = shift;

	unlink ($$rInPath) if ($cleanUp);
    delete $opts{out};
}


#--------------------------------------------------------------------------

# Parse args

# No args at all
$opts{help} = 1 unless @ARGV;

Getopt::Long::Configure ("bundling");
GetOptions(\%opts,
	"id3v2|2",
	"append|a",
	"both|b",
	"commentfile|c=s",
	"edit|e",
	"genre|g",
	"kill|k",
	"list|l",
    "lowercase|L",
    "modify|m",
	"out|o=s",
	"tag|t=s",
	"write|w",
	"help|h",
) or die "\n$usage\n";

# User requested help
die "$usage\n" if $opts{help};

# No switches, user wants --list behavior
$opts{list} = 1 if (keys %opts < 2);

# Parse tags input
if ($opts{tag}) {
    for my $full (@{$opts{tag}}) {
        $full =~ /(.*)=(.*)/;
        $tags{$1} = $2;
    }
}

# Remaining args should be file paths
@inPaths = @ARGV;

if ((@inPaths > 1) && ($opts{out})) {
    die <<MSG;
Output file specified along with multiple input files. This input doesn't make 
sense, aborting.

MSG
}

loadCommentFile($opts{commentfile}) if $opts{commentfile};


#Debugging
if(DEBUG) {
	print "--\n";
	print "opts{id3v2}: ", $opts{id3v2} || "[undefined]", "\n";
	print "opts{append}: ", (defined $opts{append} ? $opts{append} : "[undefined]"), "\n";
	print "opts{both}: ", (defined $opts{both} ? $opts{both} : "[undefined]"), "\n";
	print "opts{commentfile}: ", (defined $opts{commentfile} ? $opts{commentfile} : "[undefined]"), "\n";
	print "opts{edit}: ", (defined $opts{edit} ? $opts{edit} : "[undefined]"), "\n";
	print "opts{genre}: ", (defined $opts{genres} ? $opts{genres} : "[undefined]"), "\n";
	print "opts{kill}: ", (defined $opts{kill} ? $opts{kill} : "[undefined]"), "\n";
	print "opts{list}: ", (defined $opts{list} ? $opts{list} : "[undefined]"), "\n";
	print "opts{lowercase}: ", (defined $opts{lowercase} ? $opts{lowercase} : "[undefined]"), "\n";
	print "opts{modify}: ", (defined $opts{modify} ? $opts{modify} : "[undefined]"), "\n";
	print "opts{out}: ", (defined $opts{out} ? $opts{out} : "[undefined]"), "\n";
    if ($opts{tag}) {
        print "opts{tag}\n";
        for my $tag (@{$opts{tag}}) {
            print "   $tag\n";
        }
    }

	print "opts{write}: ", (defined $opts{write} ? $opts{write} : "[undefined]"), "\n";
	print "inPaths: [count " . scalar(@inPaths) . "] @inPaths\n";
	print "--\n";

    exit 1 if (DEBUG == 2);
}

# User just wants to see the genres
if($opts{genre}) {
	my @genres = sort @{SongInfo::MP3::genres()};

    my $colSize = int (@genres / 3);
    for (my $i = 0; $i < $colSize; $i++) {
        printf "%-26s%-26s%-26s\n", $genres[$i], $genres[$i + $colSize], 
            ($i + (2 * $colSize) < @genres) ? 
                ($genres[$i + (2 * $colSize)]) : '';
    }

	exit 0;
}

# For everything else we need an input path
die "At least one input file must be specified\n\n$usage\n" unless @inPaths;

# Do all this for each input file given
for my $inPath (@inPaths) {
    if (-d $inPath) {
        warn "$inPath is a directory. Skipping.\n";
        next;
    }

    # Figure out what to do based on the switches
    if($opts{list}) { showTags($inPath); }
    elsif($opts{id3v2}) { showV2Tags($inPath); }
    elsif($opts{both}) {
        print "ID3v1 tags\n";
        showTags($inPath);
        print "\nID3v2 tags\n";
        showV2Tags($inPath);
    }
    elsif(($opts{append}) || ($opts{modify}) || ($opts{write})) {
        beforeNoOutfile(\$inPath);
        editTags($inPath, $opts{out});
        afterNoOutfile(\$inPath);
    }
    elsif($opts{lowercase}) {
        beforeNoOutfile(\$inPath);
        lcTags($inPath, $opts{out});
        afterNoOutfile(\$inPath);
    }
    elsif($opts{edit}) {
        # Get a temp file
        my $tmpDir = File::Spec->tmpdir || '/tmp';
        my ($fh, $filename) = tempfile(DIR => $tmpDir);
        my $timestamp = (stat $filename)[9];

        # Make comment file
        showTags($inPath, $fh);

        # Edit file
        my $editor = $ENV{EDITOR} || 'vim';
        system "$editor $filename";

        # Figure out if file changed
        if ((stat $filename)[9] > $timestamp) {
            beforeNoOutfile(\$inPath);

            loadCommentFile($filename);
            editTags($inPath, $opts{out});

            afterNoOutfile(\$inPath);
        }

        # Get rid of temp file
        close $fh;
        unlink $filename;
    }
    elsif($opts{kill}) {
        beforeNoOutfile(\$inPath);
        killV2Tags($inPath, $opts{out});
        afterNoOutfile(\$inPath);
    }
}
