#!/usr/bin/perl -w
my $RCS_Id = '$Id: ls2nocddb.pl,v 1.12 2004/08/13 13:42:27 jv Exp $ ';

# Author          : Johan Vromans
# Created On      : Tue Sep 15 15:59:04 1992
# Last Modified By: Johan Vromans
# Last Modified On: Fri Aug 13 15:41:52 2004
# Update Count    : 115
# Status          : Unknown, Use with caution!

################ Common stuff ################

use strict;

# Package or program libraries, if appropriate.
# $LIBDIR = $ENV{'LIBDIR'} || '/usr/local/lib/sample';
# use lib qw($LIBDIR);
# require 'common.pl';

# Package name.
my $my_package = 'Sciurix';
# Program name and version.
my ($my_name, $my_version) = $RCS_Id =~ /: (.+).pl,v ([\d.]+)/;
# Tack '*' if it is not checked in into RCS.
$my_version .= '*' if length('$Locker:  $ ') > 12;

use FindBin;
use lib $FindBin::Bin;

################ Command line parameters ################

use Getopt::Long 2.13;

# Command line options.
my $length = -1;		# calculate length of tracks
my $verbose = 0;		# verbose processing
my $print = 0;			# send to stdout

# Development options (not shown with -help).
my $debug = 0;			# debugging
my $trace = 0;			# trace (show process)
my $test = 0;			# test mode.

# Process command line options.
app_options();

# Post-processing.
$trace |= ($debug || $test);

################ Presets ################

my $TMPDIR = $ENV{TMPDIR} || $ENV{TEMP} || '/usr/tmp';

################ The Process ################

use Cwd qw(abs_path);

foreach my $dir ( @ARGV ) {

    unless ( -d $dir ) {
	warn("$dir: not a directory -- skipped\n");
	next;
    }
    if ( -s "$dir/.nocddb" && !$print ) {
	warn("$dir/.nocddb: exists -- directory skipped\n");
	next;
    }

    $dir =~ s;/+$;;;
    my @a = split('/', abs_path($dir));
    my ($artist, $album) = ("","");
    if ( @a >= 2 ) {
	$artist = neat($a[-2]);
	$album = neat($a[-1]);
    }

    my @files = sort map { $1 if /([^\/]+)\.mp3$/ } glob("$dir/[0-9]*.mp3");

    my $fh = *STDOUT;
    unless ( $print ) {
	open($fh, ">$dir/.nocddb") or die("$dir/.nocddb: $!\n");
    }
    print $fh ("$artist / $album\n\n");

    my $prev = 0;
    my @res;
    my $maxlen = 40;
    foreach my $file ( @files ) {
	my $mmss = 0;
	$mmss = getlength("$dir/$file.mp3") if $length;
	$file =~ s/^(\d+)_(-_)?//;
	$prev++;
	if ( $1 - $prev > 5 ) {
	    warn("Too many 'Unknown's, suppressing\n");
	    $prev = $1;
	}
	my $t;
	while ( $prev < $1 ) {
	    $t = sprintf ("  %2d. %s", $prev, "Unknown");
	    $maxlen = length($t) if $maxlen < length($t);
	    push(@res, [ $t, 0 ]);
	    $prev++;
	}
	$t = sprintf ("  %2d. %s", $1, neat($file));
	$maxlen = length($t) if $maxlen < length($t);
	push(@res, [ $t, $mmss ]);
    }

    foreach ( @res ) {
	my ($t, $mmss) = @$_;
	my $l = length($t);
	unless ( $mmss ) {
	    print $fh ($t, "\n");
	    next;
	}
	$t .= "\t";
	$l = ($l & 0xf8) + 7;
	while ( $l <= $maxlen ) {
	    $t .= "\t";
	    $l += 8;
	}
	print $fh ($t, $mmss, "\n");
    }

    print $fh ("\nGenerated by $my_name $my_version on ".
	       localtime(time)."\n");
    close($fh);

}

exit 0;

################ Subroutines ################

sub neat {
    local($_) = shift;
    s/__/:_/g;
    s/;_/_\/_/g;
    s/_s_/'s_/gi;
    s/_m_/'m_/gi;
    s/(you|we|they)_re_/$1're_/gi;
    s/_/ /g;
    s/\s+/ /g;
    s/(^|-|\s|\(|\[)(.)/$1.ucfirst($2)/eg;
    $_;
}

sub getlength {
    my ($file) = @_;
    my $mmss;
    if ( $length < 0 ) {
	$mmss = getlength_mi($file);
	return $mmss if defined $mmss;
    }
    $mmss = getlength_af($file);
    return $mmss if defined $mmss;
    $length = 0;
    return " -:--";
}

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

    unless ( eval { require MP3::Info; } ) {
	warn("Cannot load MP3::Info\n");
	return undef;
    }
    import MP3::Info;

    return " -:--" unless -f $file;

    my $f = get_mp3info($file);
    return " -:--" unless $f;

    sprintf("%2d:%02d", $f->{MM}, $f->{SS});
}

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

    unless ( eval { require MPEG::Audio::Frame; } ) {
	warn("Cannot load MPEG::Audio::Frame\n");
	return undef;
    }

    return " -:--" unless -f $file;

    my $f;
    unless ( open($f, '<', $file) ) {
	warn("$file: $!\n");
	return " -:--";
    }

    my $length;
    my $frames = 0;
    while ( my $frame = MPEG::Audio::Frame->read($f)) {
	$length += $frame->seconds;
    }
    return dptime_formatted($length);
}

sub dptime_formatted {
    my ($t) = (@_);
    $t += 0.5;
    $t = int($t);
    my $s = $t % 60;
    my $m = int($t / 60);
    sprintf("%2d:%02d", $m, $s);
}

################ Subroutines ################

sub app_options {
    my $help = 0;		# handled locally
    my $ident = 0;		# handled locally

    # Process options, if any.
    # Make sure defaults are set before returning!
    return unless @ARGV > 0;

    if ( !GetOptions(
		     'length!'	=> \$length,
		     'ident'	=> \$ident,
		     'print'	=> \$print,
		     'verbose'	=> \$verbose,
		     'trace'	=> \$trace,
		     'help|?'	=> \$help,
		     'debug'	=> \$debug,
		    ) or $help )
    {
	app_usage(2);
    }
    app_ident() if $ident;
}

sub app_ident {
    print STDERR ("This is $my_package [$my_name $my_version]\n");
}

sub app_usage {
    my ($exit) = @_;
    app_ident();
    print STDERR <<EndOfUsage;
Usage: $0 [options] [file ...]
    -[no]length		[do not] calculate playing time
    -help		this message
    -ident		show identification
    -verbose		verbose information
EndOfUsage
    exit $exit if defined $exit && $exit != 0;
}

=head1 NAME

ls2nocddb - produce CDDB::Fake info from a directory

=head1 SYNOPSIS

    ls2nocddb [ options ] directory ...

Options:

    --[no]length [do not] include length information
    --print	 write to standard output
    --verbose    more verbose information
    --help       help message

=head1 DESCRIPTION

This program can generate CDDB::Fake information data from a directory
containing MP3 files.

It requires the directory to be named Artist_Name/Album_Title.

The MP3 files in the directory must be named NN_Track_Title.mp3, where
I<NN> is the track number.

When the package MP3::Info or MPEG::Audio::Frame is available, the
program can add the lengths of the tracks to the generated
information.

For example: Given the directory Jazz/Dick_Onstenk/Dick's_Jazz_Stuff
with the following files:

    01_Body_And_Soul.mp3
    02_Fly_Me_To_The_Moon.mp3
    03_Lover_Man.mp3
    04_Freddie_Freeloader.mp3
    05_Billie's_Bounce.mp3
    06_Softly_As_In_A_Morning_Sunrise.mp3

Then running B<ls2nocddb> with option C<--length> and argument
C<Jazz/Dick_Onstenk/Dick's_Jazz_Stuff>, will create a file named
C<.nocddb> in that directory, with the following contents:

    Dick Onstenk / Dick's Jazz Stuff

	 1. Body And Soul			 3:17
	 2. Fly Me To The Moon			 4:20
	 3. Lover Man				 6:16
	 4. Freddie Freeloader			 5:32
         5. Billie's Bounce			 4:22
         6. Softly As In A Morning Sunrise	 3:55

    Generated by ls2nocddb 1.4 on Fri Jul 25 14:31:41 2003

Note that calculating the length using MPEG::Audio::Frame make tay
some time but it is accurate. Using MP3::Info is fast but depends on
the accuracy of the MP3 header info. For this reason, MP3::Info is
preferred and MPEG::Audio::Frame is only used as a fall back.
Specifying B<--length> forces length calculation with
MPEG::Audio::Frame. B<--nolength> suppresses length calculation.

=head1 SEE ALSO

L<CDDB::File>.

=head1 DEPENDENCIES

C<MP3::Info> or C<MP3::Audio::Frame> (both optional). If installed, the
program can add the lengths of the tracks to the generated
information.

=head1 AUTHOR

Johan Vromans <jvromans@squirrel.nl>

=head1 COPYRIGHT

This programs is Copyright 2003,2004 Squirrel Consultancy.

This program is free software; you can redistribute it and/or modify
it under the terms of the Perl Artistic License or 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.

=cut