#!/usr/bin/perl
use MP3::Tag;
use MP3::Info qw(:all);
use Getopt::Long;
use Pod::Usage;
use DBI;
use Cwd qw(realpath);
use strict;
use utf8;
use open ':std', ':encoding(UTF-8)';


my $cfgdir		= sprintf('%s/.pjam', $ENV{HOME});
my $cfgfile		= sprintf('%s/config', $cfgdir);
my $pjam_version	= "1.1.1";
my $pjam_desc		= "\nid3fix version $pjam_version for PerlJammer\n";
my (%opts, $DBH, $fh);

Getopt::Long::Configure ("bundling_override");

if (scalar @ARGV == 0)
{
    pod2usage(-message => "$pjam_desc\nInsufficient arguments specified\n",
              -exitstatus => -1,
              -verbose => 0);
}
elsif (-d $cfgdir && -f $cfgfile)
{
    %opts = get_prefs($cfgfile);

    if (GetOptions(\%opts,
                   'quiet|q',
                   'check|c',
                   'duplicate|d',
                   'errorfile|e=s',
                   'help|usage|h|u|?',
                   'man|m',
                   'verbose|v',
                   'version|V'))
    {
        if ($opts{version})
        {
            pod2usage(-message => $pjam_desc,
                    -exitstatus => 0,
                    -verbose => 0);
        }
        elsif ($opts{'help'})
        {
            print "\nid3fix, an MP3 tag update tool\n";
            pod2usage(-exitstatus => 0,
                      -verbose => 1);
        }
        elsif ($opts{'man'})
        {
            pod2usage(-exitstatus => 0,
                      -verbose => 2);
        }
        else
        {
            MP3::Tag->config(write_v24 => 1, prohibit_v24 => 0);
            use_winamp_genres();

            $DBH = open_db();

            if ($opts{'errorfile'})
            {
                my $f = $opts{'errorfile'};
                open ($fh, ">$f");
            }
            while (@ARGV)
            {
                process(shift);
            }
            $DBH->disconnect();
            close($fh) if ($opts{'errorfile'});
        }
    }
    else
    {
        pod2usage(-message => $pjam_desc,
                  -exitstatus => -1,
                  -verbose => 0);
    }
}
else
{
    print "The config file ~/.pjam/config was not found.  Please set up PerlJammer\n",
          "and its database correctly before running id3sort.\n";
}

exit (0);


sub get_prefs
{
    my %o;

    open(PREFS, $_[0]);
    while (<PREFS>)
    {
        next if (/^#/);
        if (/^(\S+)\s*[:=]\s+(yes|true)$/i)
        {
            $o{$1} = 1;
        }
        elsif (/^(\S+)\s*[:=]\s+(no|false)$/i)
        {
            $o{$1} = 0;
        }
        elsif (/^(\S+)\s*[:=]\s+"?([^"]+)"\s*$/)        # " reset
        {
            $o{$1} = $2;
        }
        elsif (/^(\S+)\s*[:=]\s+(\S.*\S)\s*$/)
        {
            $o{$1} = $2;
        }
        elsif (/^(\S+)\s*[:=]\s+(\d+)\s*$/)
        {
            $o{$1} = $2;
        }
    }

    return(%o);
}


sub open_db
{
    my ($dsn, $dbh, $user, $password);

    $dsn = "DBI:mysql:%s;"
         . "mysql_read_default_group=%s;"
         . "mysql_read_default_file=%s;"
         . "mysql_mysql_client_found_rows=TRUE;"
         . "mysql_mysql_ssl=TRUE";


    $dsn = sprintf($dsn,
                   $opts{sqldb},
                   $opts{sqlgroup},
                   $opts{sqlcfg});

    $dbh = DBI->connect($dsn, undef, undef,
                        {RaiseError => 1, AutoCommit => 1, mysql_enable_utf8 => 1});

    my $sth = $dbh->prepare('SET NAMES utf8mb4') || die "Error:" . $dbh->errstr . "\n";
    $sth->execute || die "Error:" . $dbh->errstr . "\n";

    return ($dbh);
}


sub process
{
    my $pathname = $_[0];

    $pathname = realpath($pathname);
    if (-f $pathname)
    {
        fix_tags($pathname);
    }
    elsif (-d $pathname)
    {
        recurse_into($pathname);
    }
    else
    {
        print "Skipping $pathname: Not a plain file or directory\n" if ($opts{'verbose'});
    }
}


sub recurse_into
{
    my $path = $_[0];

    print "Entering $path\n" unless ($opts{'quiet'});

    chdir($path);

    opendir(DIR, '.');
    my @files = grep (!/^\./, readdir(DIR));
    closedir(DIR);

    foreach my $file (sort @files)
    {
        process($file);
    }
    print "Leaving $path\n" unless ($opts{'quiet'});

    chdir('..');
}

sub fix_tags
{
    my ($file) = @_;

    if ($file =~ /\.mp3$/i)
    {
        my @sqlinfo = mysql_lookup($file);

        if (scalar @sqlinfo)
        {
            if ($opts{'check'})
            {
                print "Read tags for $file\n" unless ($opts{'quiet'});
            }
            else
            {
                rewrite_tags($file, @sqlinfo);
            }
        }
        else
        {
            if ($opts{'duplicate'})
            {
                copy_tags($file) unless ($opts{'check'});
            }
            else
            {
                print "Skipping $file : Not in database\n" unless ($opts{'quiet'});
                print $fh "$file\n" if ($opts{'errorfile'});
            }
        }
    }
    else
    {
        print "Skipping $file : Not an MP3 file\n" unless ($opts{'quiet'});
    }
}


sub rewrite_tags
{
    my ($file, $title, $artist, $disc, $year, $track, $genre) = @_;
    my ($genreID, $mp3);
    
    print "rewrite_tags: RECEIVED title $title, artist $artist, disc $disc, year $year, track $track, genre $genre FROM DATABASE\n" if ($opts{'verbose'});

    $mp3 = MP3::Tag->new($file);
    $genreID = $mp3->genres($genre);

    printf ("Genre: %s, Genre ID: %d\n", $genre, $genreID) if ($opts{'verbose'});

    printf("Fixing %s ....", $file);

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

    $mp3->new_tag("ID3v1");
    $mp3->{ID3v1}->all($title,
                       $artist,
                       $disc,
                       $year,
                       'Created by Grip',
                       $track,
                       $genreID);
    $mp3->{ID3v1}->write_tag;

    $mp3->new_tag("ID3v2");
    $mp3->{ID3v2}->add_frame('TIT2', $title);
    $mp3->{ID3v2}->add_frame('TPE1', $artist);
    $mp3->{ID3v2}->add_frame('TALB', $disc);
    $mp3->{ID3v2}->add_frame('TYER', $year);
    $mp3->{ID3v2}->add_frame('TCON', $genre);
    $mp3->{ID3v2}->add_frame('TRCK', $track);
    $mp3->{ID3v2}->write_tag;

    $mp3->close;

    print " Done.\n";
}


sub copy_tags
{
    my ($file) = @_;
    my $mp3 = MP3::Tag->new($file);

    printf("Duplicating tags in %s ....", $file);

    $mp3->get_tags;
    my ($title, $track, $artist, $disc, $comment, $year, $genreID) = $mp3->autoinfo();
    my $genre = $mp3->genres($genreID);

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

    $mp3->new_tag("ID3v1");
    $mp3->{ID3v1}->all($title,
                       $artist,
                       $disc,
                       $year,
                       $comment,
                       $track,
                       $genreID);
    $mp3->{ID3v1}->write_tag;

    $mp3->new_tag("ID3v2");
    $mp3->{ID3v2}->add_frame('TIT2', $title);
    $mp3->{ID3v2}->add_frame('TPE1', $artist);
    $mp3->{ID3v2}->add_frame('TALB', $disc);
    $mp3->{ID3v2}->add_frame('TYER', $year);
    $mp3->{ID3v2}->add_frame('TCON', $genre);
    $mp3->{ID3v2}->add_frame('TRCK', $track);
    $mp3->{ID3v2}->write_tag;
    $mp3->close;

    print " Done.\n";
}


sub mysql_lookup
{
    my ($file) = @_;
    my @sqlinfo = ();

    my $STH = $DBH->prepare(sprintf('SELECT s.title,a.artist,d.disc,s.year,s.track_num,s.genre from artist=a,disc=d,song=s where s.artistid=a.id and s.discid = d.id and s.filename="%s"',
                                    $file));
    $STH->execute || die "Error:" . $DBH->errstr . "\n";
    while (my $ref = $STH->fetchrow_arrayref)
    {
        @sqlinfo = ($$ref[0], $$ref[1], $$ref[2], $$ref[3], $$ref[4], $$ref[5]);
    }
    $STH->finish();

    printf ("mysql_lookup: READ title %s, artist %s, disc %s, year %d, track %d, genre %s FROM DATABASE\n", @sqlinfo) if ($opts{'verbose'});

    return (@sqlinfo);
}



__END__

=head1 NAME

B<id3fix> - Rewrite ID3 tags based on the PerlJammer SQL database

=head1 VERSION

Version 1.1.1

=head1 SYNOPSIS

id3fix [options] files

=head1 OPTIONS

=over 4

=item B<-c, --check>

Do not update tags; lookup only

=item B<-d, --duplicate>

Duplicate id3v1 to id3v2 tags and vice versa

=item B<-q, --quiet>

No status messages; report only lookup failures

=item B<-e, --errorfile <file>>

Log database lookup failures to <file>

=item B<--help, --usage, -h, -u, -?>

List command-line options and usage, then exit

=item B<-m, --man>

Display full documentation and exit

=item B<-v, -verbose>

Display additional diagnostic information

=item B<-V, -version>

Display version string and exit
All options can be abbreviated to their first letters.

=back

=head1 DESCRIPTION

B<id3fix> scans a single or multiple files or directories looking for MP3
files, descending recursively into directories.  When it finds MP3 files,
it connects to DigitalDJ's database to look up the file.  If the file is
not found in the database, B<id3tag> reports an error.  If the file is
found in the database, B<id3tag> purges all existing MP3 tags from the file
and rewrites new ID3V1 and ID3v2 tags using the information from the database,
unless the B<-check> option was specified instructing B<id3tag> to check only
and do no rewriting.

Optionally, for files not found in the database, id3fix can duplicate the
contents of id3v1 and id3v2 tags to each other in order to place the most
complete data available in both sets of tags.  To specify this behavior, use
the -duplicate option.  Using -duplicate makes -errorfile moot.

Normally, B<id3tag> reports each time it enters or leaves a directory, each
file it fixes, and any files it skips because the file is (in order of
testing) not a plain file or directory, not an MP3 file, or not found in the
DigitalDJ database.  The -quiet option causes all of these messages to be
suppressed except for database lookup failures.

=head1 REPORTING BUGS

Please send all bug reports to the author.

=head1 LICENSE

B<id3fix>, part of the B<PerlJammer> package, is copyright (C) 2003 Phil Stracchino.

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 3 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, see <http://www.gnu.org/licenses/>.

=head1 OBTAINING PerlJammer

B<PerlJammer> can be downloaded from http://co.ordinate.org/perljammer/.

=head1 AUTHOR

B<PerlJammer> and its supporting tools are written and maintained by Phil Stracchino
(phil@co.ordinate.org).

=cut
