#!/usr/bin/perl
use Getopt::Long;
use Pod::Usage;
use MP3::Tag;
use MP3::Info qw(:all);
use Cwd qw(realpath);
use File::Copy qw(mv cp);
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		= "\nid3sort version $pjam_version for PerlJammer\n";

my %opts;


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);

    Getopt::Long::Configure ("bundling");
    if (GetOptions(\%opts,
                   'basedir|b=s',
                   'help|usage|?|h|u',
                   'pretend|p',
                   'quiet|q',
                   'tagged|t',
                   'untagged|u',
                   'man|m',
                   'version|v'))
    {
        if ($opts{version})
        {
            pod2usage(-message => $pjam_desc,
                      -exitstatus => 0,
                      -verbose => 0);
        }
        elsif ($opts{help})
        {
            pod2usage(-message => $pjam_desc,
                      -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();
        
            while (@ARGV)
            {
                process(shift);
            }
        }
    }
    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>)
    {
        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 process
{
    my $pathname = $_[0];

    $pathname = realpath($pathname);
    if (-f $pathname)
    {
        read_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 read_tags
{
    my ($file) = @_;
    my (@terms, $mp3, $tags, $newpath);

    if ($file =~ /\.mp3$/i)
    {
        my $basepath = realpath('.');

        $mp3 = MP3::Tag->new($file);
        $mp3->get_tags;

        my ($title, $track, $artist, $disc, $comment, $year, $genre) = $mp3->autoinfo;
        my $info = get_mp3info($file) || warn "\t***\tCould not retrieve MP3 metadata from $file";
        my $genreID = $mp3->genres($genre);
        $mp3->close;

        my ($a, $d, $t) = ($artist, $disc, $title);

        $a =~ tr/A-Za-z0-9_\.'!/_/cs;
        $d =~ tr/A-Za-z0-9_\.'!/_/cs;
        $t =~ tr/A-Za-z0-9_\.'!/_/cs;

        $newpath = sprintf('%s/%s/%s/%s.mp3',
                           $opts{basedir} ? $opts{basedir} : '.',
                           $a || 'UNKNOWN',
                           $d || 'UNKNOWN',
                           $t || substr($file, rindex($file,'/')));

         $newpath =~ s/'//g;

        if (($opts{untagged} && !($artist && $title && $disc))
         || ($opts{tagged} && ($artist && $title && $disc))
         || (!$opts{tagged} && !$opts{untagged}))
        {
            printf("%s\n\t-> %s\n\t\t=> %s\n",
                   substr($file, length($basepath)+1),
                   sprintf('%s :: %s :: %s', $artist, $disc, $title),
                   $newpath) unless ($opts{quiet});
        
            unless ($opts{pretend})
            {
                my $path = substr($newpath, 0, rindex($newpath, '/'));
                system('mkdir', '-p', $path) unless (-d $path);
                print "Moving $file to $path\n";
                mv ($file, $newpath);
            }
        }
    }
    else
    {
        print "Skipping $file : Not an MP3 file\n" unless ($opts{quiet});
    }
}


__END__

=head1 NAME

B<id3sort> - Sort unsorted mp3 files according to their id3 tags.

=head1 VERSION

Version 1.1.1

=head1 SYNOPSIS

id3sort [options] file|path [...]

  Options:
    --basedir
    --pretend
    --tagged
    --untagged
    --quiet
    --help, --usage, -?
    --man, --manual
    --version
 
=head1 OPTIONS

=over 4

=item B<-b, --basedir>

Use B<basedir> as the target location to store files under.

=item B<-p, --pretend>

Don't actually move anything, just report what would be done

=item B<-t, --tagged>

Process only files for which all three principal tag fields
(artist, disc, title) are present

=item B<-u, --untagged>

Process only files for which at least one of the artist, disc,
or title tags are missing (normally used only with --pretend).

=item B<-q, --quiet>

Skip informational status messages.

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

List command-line options and usage, then exit

=item B<-m, --man, --manual>

Display full documentation and exit

=item B<-v, --version>

Display version string and exit

=back

=head1 DESCRIPTION

B<id3sort> retrieves id3 tag metadata from files as specified on the
command line, constructs canonical filenames for them based on the
contents of the artist/disc/title tags, and optionally moves the files
to those locations.  If the B<--pretend> option is used, it will
report what it would do without actually changing anything; this is a
good way to check that the metadata is sane before actually committing
changes.  Optionally, the B<--tagged> and B<--untagged> options can be
used to filter files depending on whether or not all three of the artist,
disc, and title tags are present.

=head1 REPORTING BUGS

Please send all bug reports to the author.

=head1 LICENSE

B<id3sort>, 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
