#!/usr/bin/perl -w
# (C) 2003-2007 Willem Jan Hengeveld <itsme@xs4all.nl>
# Web: http://www.xs4all.nl/~itsme/
#      http://wiki.xda-developers.com/
#
# $Id$
#
# todo:
#    add option to apply 's/(xxx)..(yyy)/sprintf("%s-%s", $1, $2)/e'
#    type substitution patterns.
#
use MP3::Tag;
use IO::File;
use IO::Dir;
$|=1;
use Getopt::Long;
my %g_newinfo;
my $g_verbose=0;
my $g_recurse=0;
my $g_show_fields;
my $g_import_pattern;

my %g_types= (
    t => { pattern=>qr/.*?/, name=>"title", format=>"%-30.30s" },
    a => { pattern=>qr/.*?/, name=>"artist", format=>"%-20.20s" },
    l => { pattern=>qr/.*?/, name=>"album", format=>"%-30.30s" },
    y => { pattern=>qr/\d+/, name=>"year", format=>"%-4.4s" },
    g => { pattern=>qr/.*?/, name=>"genre", format=>"%-10.10s" },
    c => { pattern=>qr/.*?/, name=>"comment", format=>"%-30.30s" },
    n => { pattern=>qr/\d+/, name=>"track", format=>"%-3.3s" },
);

sub usage {
    return <<__EOF__
Usage: mp3tag [-v] [-f FIELDS] -FIELD VALUE [files | wildcards]
    -v        : g_verbose more -v -> more g_verbose
    -r        : g_recurse into subdirectories, processing *.mp3 files
    -i PATTERN : import from path+filename
              EXAMPLE: "\%a \%y - \%l/\%n - \%t.mp3"
              will try to extract the specified fields from the filename.
    -f FIELDS : what fields (from a,y,l,...) to list in the output
                or * for 'aylnt'
    -a VALUE  : set artist name
    -y VALUE  : set year
    -l VALUE  : set album title
    -n VALUE  : set tracknumber
    -t VALUE  : set song title
    -g VALUE  : set genre
    -c VALUE  : set comment
__EOF__
}
GetOptions(
    "t=s"=> sub { $g_newinfo{t}= $_[1]; },
    "a=s"=> sub { $g_newinfo{a}= $_[1]; },
    "l=s"=> sub { $g_newinfo{l}= $_[1]; },
    "y=s"=> sub { $g_newinfo{y}= $_[1]; },
    "g=s"=> sub { $g_newinfo{g}= $_[1]; },
    "c=s"=> sub { $g_newinfo{c}= $_[1]; },
    "n=s"=> sub { $g_newinfo{n}= $_[1]; },
    "i=s"=> \$g_import_pattern,
    "f=s"=> \$g_show_fields,
    "v+" => \$g_verbose,
    "r"  => \$g_recurse,
) or die usage();

die usage() if (!@ARGV) ;

$g_show_fields= "*" if (!$g_import_pattern && !%g_newinfo);

$g_show_fields= "aylnt" if ($g_show_fields && $g_show_fields eq "*");
my ($g_pathpattern, $g_pathvars)= parse_import_pattern($g_import_pattern) if ($g_import_pattern);


for my $arg (@ARGV)  {
    if ($arg =~ /\*/) {
        for my $filename (glob( $arg =~ /\// ? "\"$arg\"": $arg)) {
            processFile($filename);
        }
    }
    elsif (-d $arg) {
        processDirectory($arg);
    }
    elsif (-f $arg) {
        processFile($arg);
    }
    else {
        warn "'$arg' is neither a file or directory\n";
    }
}
exit(0);
sub parse_import_pattern {
    my ($pattern)= @_;
    my $re;
    my @vars;
    for my $part (split /(%\w|[\/\\]|\.)/, $pattern) {
        if ($part =~ /^%(\w)$/) {
            my $type= $1;
            if (!exists $g_types{$type}) {
                die "invalid type '$type' specified in import pattern\n";
            }
            $re .= "($g_types{$type}{pattern})";
            push @vars, $type;
        }
        elsif ($part =~ /^[\/\\]$/) {
            $re .= qr/[\/\\]/;
        }
        elsif ($part eq ".") {
            $re .= qr/\./;
        }
        else {
            $re .= $part;
        }
    }
    return ($re, \@vars);
}
sub processDirectory {
    my @subdirs= @_;

    while (@subdirs) {
        my $dirname= shift @subdirs;
        my $dir= IO::Dir->new($dirname);

        while (my $entry= $dir->read()) {
            next if ($entry eq "." || $entry eq "..");

            my $fullpath= "$dirname/$entry";

            if (-f $fullpath && $entry =~ /\.mp3$/i) {
                processFile($fullpath);
            }
            elsif ($g_recurse && -d $fullpath) {
                push @subdirs, $fullpath;
            }
        }
        $dir->close();
    }
}
sub processFile {
    my ($fn)= @_;

    my %taginfo;

    my $mp3= MP3::Tag->new($fn);
    if (!$mp3) {
        warn "$fn: $!\n";
        return;
    }

    # get info from path+filename
    $taginfo{filename}= parse_filename($fn) if ($g_import_pattern);

    $mp3->get_tags;

    # get id3v1 info
    if (!exists $mp3->{ID3v1}) {
        $mp3->new_tag("ID3v1");
    }
    my $id3v1= $mp3->{ID3v1};
    $taginfo{id3v1}= extract_id3_info($id3v1);

    # get id3v2 info
    if (!exists $mp3->{ID3v2}) {
        $mp3->new_tag("ID3v2");
    }
    my $id3v2= $mp3->{ID3v2};
    $taginfo{id3v2}= extract_id3_info($id3v2);


    # update id3v1, and id3v2 info
    for my $tags ($id3v1, $id3v2) {
        my $updatecount=0;
        for my $key (keys %g_newinfo) {
            my $fieldname= $g_types{$key}{name};
            $tags->$fieldname($g_newinfo{$key});

            $updatecount++;
        }
        for my $key (keys %{$taginfo{filename}}) {
            my $fieldname= $g_types{$key}{name};
            $tags->$fieldname($taginfo{filename}{$key});

            $updatecount++;
        }
        if ($updatecount) {
            $tags->write_tag;
        }
    }

    displayfields(\%taginfo, $fn) if ($g_show_fields);

    print "$fn\n\n" if ($g_verbose);
    writeinfo(\%taginfo) if ($g_verbose==1);
    writeid3v2($id3v2) if ($g_verbose>1);
    writeid3v1($id3v1) if ($g_verbose>1);
    print "\n\n" if ($g_verbose);
}
sub parse_filename {
    my ($fn)= @_;
    my %info;

    if (my @fields = ($fn =~ $g_pathpattern)) {
        return { map { ($g_pathvars->[$_] => $fields[$_]) } (0..$#fields) };
    }
    else {
        warn "filename did not match pattern: $fn\n";
        return {};
    }
}
sub displayfields {
    my ($info, $fn)= @_;

    printf("%s : %s\n", join(", ", map { sprintf($g_types{$_}{format}, $info->{id3v2}{$_} || $info->{id3v1}{$_}); } (split //, $g_show_fields)), $fn);
}
sub writeinfo {
    my ($info)= @_;

    for my $key (keys %g_types) {
        printf("%-10s: %-30s %s\n", $g_types{$key}{name}, $info->{id3v1}{$key}, $info->{id3v2}{$key});
    }
}
sub extract_id3_info {
    my ($id3)= @_;
    my %info;
    for my $key (keys %g_types) {
        my $fieldname= $g_types{$key}{name};
        $info{$key}= $id3->$fieldname || "";
    }
    return \%info;
}
sub writeid3v1 {
    my ($id3v1)= @_;
    for my $k (keys %$id3v1) {
        printf("   id3v1: %s => %s\n", $k, $id3v1->{$k});
    }
}
sub writeid3v2 {
    my ($id3v2)= @_;
    my $fids= $id3v2->get_frame_ids('truename');
    for my $f (keys %$fids) {
        my ($info, $name, @rest) = $id3v2->get_frame($f);
        for my $i ($info, @rest) {
            if (ref $i) {
                printf("   id3v2: %s => %s [ hash ]\n", $f, $name);
                for my $k (keys %$i) {
                    printf("          %s => %s\n", $k, $i->{$k});
                }
            }
            elsif (defined $i) {
                printf("   id3v2: %s => %s => %s\n", $f, $name, $i);
            }
            else {
                printf("   id3v2: %s => %s [ undef ]\n", $f, $name);
            }
        }
        if (!defined $info && !@rest) {
            printf("   id3v2: %s  [ no info ]\n", $f);
        }
    }
}

