#!/usr/bin/perl

# AudioLink
#
# $Id: alfilldb,v 1.33 2003/12/05 12:31:42 amitshah Exp $
#
# alfilldb: Implements the 1st module of the AudioLink software. This
# script crawls through a local collection of music files and
# populates the database based on the tag information found  in the
# files.
#
# Copyright (C) 2003, Amit Shah <amitshah@gmx.net>
#
# This file is part of AudioLink.
#
# AudioLink 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; version 2 of the License, or
# (at your option) any later version.
#
# AudioLink 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 AudioLink; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

use DBI;
use MP3::Info;
use File::Find;
use Getopt::Long;
use Ogg::Vorbis::Header;
use Pod::Usage;

# Options
$help = 0;    # Display usage information and quit.
$verbose = 0; # Some extra information to be displayed

$no_act = 0;  # Do not put the stuff in the database
$prompt = 0;  # Prompt for user input if there isn't enf info in ID3 or Vorbis
$dontbug = 0; # Don't prompt if title is NULL. Skips song addition in
	      # the database in this case.


$addmode = 0; # If the script was invoked as add-only (or w/o add-only
	      # and upd-only) (new song entries in the datbase
	      # allowed).
$updmode = 0; # If the script was invoked as upd-only (or w/o add-only
	      # and upd-only) (update existing entries in the
	      # database).
$updsong = 0; # Update the song info (ID3 tags for MP3, Vorbis comments).

$mp3_ogg = 0; # 1 = MP3, 2 = Ogg Vorbis

@files;       # Multiple pathnames given on the command prompt
$file = "";   # Pathname given on the command prompt to be operated on.

@guessed_parts; # parts of the guessed names: artist, album, etc.
$guessed = 0;   # have we shown the 'guesses' list already?
@guessed_nr;    # [0]: number of guessed values; [1]: 0+tags

$update_db = 0; # If there exists an entry in the database for any
		# song, we need to update it.

# Options for the database
$user = undef;
$password = undef;
$host = "localhost"; # use local mysql server by default

# show the tags read from the file and the db.
sub show_file_tags {
    my $counter = $guessed_nr[0];

    while ($counter <= $guessed_nr[1]) {
	print "\t\t[" . $counter . "] " . $guessed_parts[$counter++] . "\n";
    }
}

# guess the values of artist, album, etc. from the filenames
# parameter: the filename
sub guess_guessed {
    # just look between '-' for possible values.
    @guessed_parts = split(/-/, $_[0]);

    my $part;
    my $counter = 0;

    foreach $part (@guessed_parts) {
	# strip leading and trailing whitespaces
	$guessed_parts[$counter++] =~ s/(^\s*)(((\w+)(\s*))*(\w+))(\s*$)/$2/;

	# 2 stupid bugs above:
	# emacs: emacs interprets the ending $) as a variable and
	#   doesn't have a closing bracket to get the indentation right
	# perl: if a space is put between $ and ), the regexp doesn't work
    }
}


# print the guessed values of artist, album, etc.
sub show_guessed {
    $guessed = 1; # we've shown this list...

    my $counter = 0;

    unless ($dontbug) {
	my $part;

	print "NOTE: Use gg<nr> to select a particular entry. Replace <nr> by the corresponding number.\n";
	print "\tChoices for fields in the song guessed from the filename are: \n";
	foreach $part (@guessed_parts) {
	    print "\t\t[$counter] $guessed_parts[$counter]\n";
	    $counter++;
	}
	$guessed_nr[0] = $counter;

	$guessed_parts[$counter++] = $album if $album;
	$guessed_parts[$counter++] = $artist if $artist;
	$guessed_parts[$counter++] = $title if $title;
	$guessed_parts[$counter++] = $comment if $comment;
	$guessed_parts[$counter++] = $genre if $genre;
	$guessed_parts[$counter++] = $year if $year;
	$guessed_parts[$counter++] = $track if $track;

	$guessed_nr[1] = $counter - 1;

	# show the entries from the db/file tags
	print "\tFields from the tags/db:\n";
	show_file_tags($guessed_nr[0]);
    }
}

# see if the field is empty. If it is, ask for a value.
# parameters: 0: the field to be compared
#             1: the name of the field.
#             2: the pathname of the song.
sub get_input_fields {
    my $input;
    my $done = 0;

    if ($_[0]) {
	return $_[0];
    }

    unless ($guessed) { show_guessed(); }
    while (not $done) {
	print "Enter " . $_[1] . " for song ". $_[2] . ": ";
	chomp($input = <STDIN>);
	if ($input =~ /^gg(\d{1,2})$/) {
	    if ($1 > $guessed_nr[1]) {
		print "Invalid input; try again\n";
		next;
	    }
	    $input = $1;
	    $done = 1; # valid guessed_field was given
	} else {
	    $done = 2; # valid input was given
	}
    }
    if ($done == 1) {
	if ($input <= $guessed_nr[1]) { # if we guessed from file name / tag
	    $input = $guessed_parts[$input];
	}
    }
    return $input;
}


# the 'find' command executes this subroutine for each file found.
sub wanted {
    my $updating = 0; # are we updating an entry?
    my $pathname;     # stores the absolute file name,
		      # ie. /path/to/file.ogg
    my $dirname;      # stores the dirname, ie., /path/to/
    my $filename;     # stores the filename, ie, file.ogg
    my @fileparts;    # stores all the parts of a file; ie, /path,
		      # /to, file.ogg.
    my $filname;      # filename w/o extension

    if ($file) {    # filename given on the command-line
	$pathname = $file;
    } else {        # directory name given on the command-line
	$pathname = $File::Find::name;
	$dirname = $File::Find::dir;
    }

    @fileparts = split(/\//, $pathname);
    $filename = $fileparts[-1]; # get the filename from the last item
    				# in the array.

    if ($filename =~ /\.mp3/i) { # case-insensitive search
	$mp3_ogg = 1;
	$filname = $`;
    }
    elsif ($filename =~ /\.ogg/i) {
	$mp3_ogg = 2;
	$filname = $`;
    }
    else {
	# supporting MP3 and Ogg Vorbis only
	return;
    }


    # the order below is important, as the fields are used in an array
    # (indexed by numbers) below. If new fields are added, put them
    # after the last field below.
    my $statement = qq(SELECT song_nr, song, album,
		       year, ma1, ma2, fa1, fa2,
		       composer, lyricist, band, genre,
		       track, comment
		       FROM aldb WHERE path = "$pathname");
    
    my $sth = $dbh->prepare($statement);
    $sth->execute or die "\n$0: Error: Can't execute search query\n";

    if ($sth->rows) {
	if($updmode) { # update the entry here
	    $updating = 1;
	} else {
	    print "Duplicate entry found; not updating\n";
	    $update_db++;
	    return;
	}
    }

    if ($mp3_ogg == 1) {  # we have an MP3 file to work on
	my $tag = get_mp3tag($pathname) or 
	    print "Couldn't get mp3 tag for file $pathname\n";

	$artist = $tag->{"ARTIST"};
	$title = $tag->{"TITLE"};
	$comment = $tag->{"COMMENT"};
	$genre = $tag->{"GENRE"};
	$year = $tag->{"YEAR"};
	$album = $tag->{"ALBUM"};
	$track = $tag->{"TRACK"};
    }
    elsif ($mp3_ogg == 2) { # we have an Ogg file to work on
	eval {
	    $ogg = Ogg::Vorbis::Header->new($pathname);
	};
	if ($@) {
	    warn "Couldn't open Vorbis file $pathname; skipping...\n";
	    return;
	}

	eval {
	    foreach my $tag ($ogg->comment_tags) {
		foreach my $field ($ogg->comment($tag)) {

		    if ($tag =~ /title/i) {
			$title = $field;
		    }
		    elsif ($tag =~ /album/i) {
			$album = $field;
		    }
		    elsif ($tag =~ /tracknumber/i) {
			$track = $field;
		    }
		    elsif ($tag =~ /genre/i) {
			$genre = $field;
		    }
		    elsif ($tag =~ /performer/i) {
			$composer = $field;
		    }
		    elsif ($tag =~ /lyricist/i) {
			$lyricist = $field;
		    }
		    elsif ($tag =~ /date/i) {
			$year = $field;
		    }
		    elsif ($tag =~ /description/i) {
			$comment = $field;
		    }
		    elsif ($tag =~ /artist/i) {
			if (not $artist) {
			    $artist = $field;
			} elsif (not $band) {
			    $band = $field;
			} elsif (not $ma1) {
			    $ma1 = $field;
			} elsif (not $ma2) {
			    $ma2 = $field;
			} elsif (not $fa1) {
			    $fa1 = $field;
			} elsif (not $fa1) {
			    $fa2 = $field;
			}
		    }
		}
	    }
	};
	if ($@) {
	    warn "WARNING: Possibly bad Vorbis file, $pathname; skipping...\n";
	    return;
	}
    }

    # Now, guess the album, artist, etc. from the filename.
    # $filname is filled in when we detected whether we had
    # an MP3 or an Ogg Vorbis file.
    guess_guessed($filname);

    if ($updating) {
	my @row = $sth->fetchrow_array;

	$title = $row[1]; $album = $row[2]; $year = $row[3];
	$ma1 = $row[4]; $ma2 = $row[5]; $fa1 = $row[6]; $fa2 = $row[7];
	$composer = $row[8]; $lyricist = $row[9]; $band = $row[10];
	$genre = $row[11]; $track = $row[12]; $comment = $row[13];
    }

    if ($verbose) {
	print "About to add/update info from $pathname:\n";
	show_guessed();
    }

    # Inserting title is mandatory 'cos we create links based on song name
    unless ($title) {
	if ($dontbug) {
	    print "No-prompt mode and no title found in audio file: skipping entry $pathname\n";
	    return;
	}
	$title = get_input_fields($title, "title", $pathname);
    }

    if ($prompt_mode > 0) { # either of 'basic', 'limited', 'most', 'paranoid' modes
	$album = get_input_fields($album, "album", $pathname);

	#FIXME: for the artist, accept also if the artist is
	# a band, ma1, ma2, fa1, fa2, etc.
	$artist = get_input_fields($artist, "artist/band", $pathname);
    }

    if ($prompt_mode > 1) { # either of 'limited', 'most', 'paranoid' modes
	$genre = get_input_fields($genre, "genre", $pathname);
	$year  = get_input_fields($year, "year", $pathname);
    }

    if ($prompt_mode > 2) { # either of 'most', 'paranoid' modes
	$composer = get_input_fields($composer, "composer", $pathname);
	$lyricist = get_input_fields($lyricist, "lyricist", $pathname);
	$comment  = get_input_fields($comment, "comment", $pathname);
    }

    if ($prompt_mode > 3) { # 'paranoid' mode
	$ma1 = get_input_fields($ma1, "Male Artist-1", $pathname);
	$ma2 = get_input_fields($ma2, "Male Artist-2", $pathname);
	$fa1 = get_input_fields($fa1, "Female Artist-1", $pathname);
	$fa2 = get_input_fields($fa2, "Female Artist-2", $pathname);
	$track = get_input_fields($track, "Track", $pathname);
    }

    unless ($no_act) {
	if ($updating) {
	    $statement = qq(UPDATE aldb SET song = "$title", album = "$album", band = "$artist", genre = "$genre", year = "$year", comment = "$comment", ma1 = "$ma1", ma2 = "$ma2", fa1 = "$fa1", fa2 = "$fa2", composer = "$composer", lyricist = "$lyricist", track = "$track"
			    WHERE path = "$pathname");
	} else {
	    $statement = qq(INSERT INTO aldb (song, album, band, path, genre, track, year, comment, ma1, ma2, fa1, fa2, composer, lyricist)
			   VALUES ("$title", "$album", "$artist", "$pathname", "$genre", "$track", "$year", "$comment", "$ma1", "$ma2", "$fa1", "$fa2", "$composer", "$lyricist"));
	}

	my $sth = $dbh->prepare($statement);
	$sth->execute or 
	    print "$0: Couldn't insert data for $pathname\n";

	if ($updsong) { # update ID3 in MP3 or comment in Ogg Vorbis
	    if ($mp3_ogg == 1 ) {
		eval {
		    set_mp3tag($pathname, $title, $artist, $album, $year, $comment, $genre, $track);
		};
		if ($@) {
		    warn "WARNING: Couldn't write info back to MP3 ID3 for $pathname.\n\tDo you have write permissions?\n";
		}
	    }
	    elsif ($mp3_ogg == 2) {
		eval {
		    $ogg->add_comments("ARTIST", $artist) if $artist;
		    $ogg->add_comments("ARTIST", $ma1) if $ma1;
		    $ogg->add_comments("ARTIST", $ma2) if $ma2;
		    $ogg->add_comments("ARTIST", $fa1) if $fa1;
		    $ogg->add_comments("ARTIST", $fa2) if $fa2;
		    $ogg->add_comments("ARTIST", $band) if $band;

		    $ogg->add_comments("TITLE", $title) if $title;
		    $ogg->add_comments("ALBUM", $album) if $album;
		    $ogg->add_comments("PERFORMER", $composer) if $composer;
		    $ogg->add_comments("LYRICIST", $lyricist) if $lyricist;
		    $ogg->add_comments("DESCRIPTION", $comment) if $comment;
		    $ogg->add_comments("DATE", $year) if $year;
		    $ogg->add_comments("GENRE", $genre) if $genre;
		    $ogg->add_comments("TRACKNUMBER", $track) if $band;

		    $ogg->write_vorbis;
		};
		if ($@) {
		    warn "WARNING: Couldn't write info back to Ogg Vorbis for $pathname.\n\tDo you have write permissions?\n";
		}
	    }
	}
    }
}

# execution starts here

# check for command-line arguments
if (not @ARGV) {
    pod2usage();
}

GetOptions(
	   'help'      => \$help,
	   'verbose'   => \$verbose,
	   'na|s'      => \$no_act,
	   'prompt=s'  => \$prompt,
	   'no-prompt' => \$dontbug,
	   'user=s'    => \$user,
	   'pass=s'    => \$password,
	   'host=s'    => \$host,
	   'add-only'  => \$addmode,
	   'upd-only'  => \$updmode,
	   'upd-song'  => \$updsong,
	   'file=s'    => \@files
	   ) or pod2usage();

if ($help) {
    pod2usage();
}

if (not @ARGV and not $files[0]) {
    print "No directory or files specified\n";
    pod2usage();
}
    
if ($prompt =~ /basic/i) {
    $prompt_mode = 1;
} elsif ($prompt =~ /limited/i) {
    $prompt_mode = 2;
} elsif ($prompt =~ /most/i) {
    $prompt_mode = 3;
} elsif ($prompt =~ /paranoid/i) {
    $prompt_mode = 4;
} elsif (not $prompt) {
    $prompt_mode = 0;
} else {
    die "\n$0: Error: Invalid argument \'$prompt\' passed to \'prompt\'.
 See the alfilldb(1) man page for more information.\n";
}

if ($addmode and $updmode) {
    die "\n$0: Error: Conflicting modes specified, please choose one of "
	. " --add-only or --upd-only\n";
}

if ($addmode) {
    $updmode = 0;
} elsif ($updmode) {
    $addmode = 0;
} else { # no option specified, default to add and update
    $addmode = $updmode = 1;
}

$config_file = "$ENV{HOME}/.audiolink/config";

if (-e $config_file) {
    open(CONFFILE, $config_file);

    # go through the config file
    while (<CONFFILE>) {
	if (/^\s*user\s*\=+\s*(\w+)/) {
	    chomp($user = $1) unless $user;
	} elsif ( /^\s*pass(word)?\s*=\s*(\w+)/) {
	    chomp($password = $2) unless $password;
	} elsif ( /^\s*host\s*=\s*(\w+)/) {
	    chomp($host = $1) unless $host;
	}
    }
} else {
    warn "WARNING: config file not found. Use the audiolink script to create one.\n";
}

# connect to the database.
$dbi_string = "DBI:mysql:aldb:$host"; # using MySQL

$dbh = DBI->connect($dbi_string,$user,$password)
    or die "\n$0: Error: Could not connect to the database!\
 Check the user, password and host fields for the MySQL connection.";

if (not $files[0]) {
    foreach $path (@ARGV) {
	# Check if the user has given a path format other than an absolute
	# path or a path that starts with ~. If yes, bail out.
	unless ($path =~ /^~/ or $path =~ /^\//) {
	    print "WARNING: pathname doesn't consist of an absolute pathname.\n";
	    print "Skipping entry $path\n";
	    next;
	}
	find (\&wanted, $path);
    }
} else { # File name specified on the command prompt
    foreach $file (@files) {

	# perl magic from perl cookbook (1st ed.), recipe 7.3
	# expand tilde in filename

	$file =~ s{ ^ ~ ( [^/]* ) }
		{ $1 ? (getpwnam($1))[7]
		      : ($ENV{HOME} || $ENV{LOGDIR} || (getpwduid($>))[7])
		}ex;

	# Check if the user has given a path format other than an absolute
	# path or a path that starts with ~. If yes, bail out.
	# Since we've already expanded the ~ case, just check against /.
	unless ($file =~ /^\//) {
	    print "WARNING: pathname doesn't consist of an absolute pathname.\n";
	    print "Skipping entry $file\n";
	    next;
	}
	wanted();
    }
}

if ($update_db > 0) {
    print "$update_db entries in the database were not updated. Use
    the --upd-only option to update them.\n";
}

=pod

=head1 NAME

alfilldb - Add/update information of music files in the AudioLink database

=head1 SYNOPSIS

B<alfilldb> [I<OPTION>]... I</path/to/songs/>...

B<alfilldb> [I<OPTION>]... I<--file=/path/to/song>...

=head1 DESCRIPTION

You can use this script to add or update information about your music
files (MP3 or Ogg Vorbis) in the AudioLink database. This information
will be used when you use the L<alsearch(1)> program to search for
particular music. This program is part of the L<audiolink(1)> package.

The path given for the location of individual files or directories
must be an absolute path (paths with ~ are allowed). Relative paths
are not allowed. See the L<"examples"> section for more information.

The user and password options have to be specified to gain access to
the database. See the "I<more information>" section in the
L<audiolink(1)> man page for the various ways in which you can specify
them.

Options that are used repetitively during different invocations of the
program (like the user, password options) may be put in the config
file. See the L<audiolink(1)> man page for details on the config file.

=head1 OPTIONS

=over

=item B<--add-only>

Only the information about new songs will be added to the database;
information about existing songs will not be updated.

=item B<--file>=I<xxx>

Works on just a single file instead of a directory. If this argument
is given, the /path/to/dirs is not considered.  (You can have more
than one of these).

=item B<--help>

Brief usage information

=item B<--host>=I<xxx>

Connects to the MySQL server on the given host. Default is localhost.

=item B<--no-prompt>

Don't prompt for anything. Songs which do not have information for
mandatory fields (eg., song title) will not be added to the database.

=item B<--pass>=I<xxx>

Password for accessing the database

=item B<--prompt>=I<xxx>

Prompt for input if there isn't enough information in the song (ID3 or
Ogg Vorbis comments).

The parameters that prompt accepts are: 

=over

=item I<basic>

Prompts just for the album name and the artist/band name

=item I<limited>

Prompts for album, artist/band, genre and year fields

=item I<most>

Prompt for all the fields except the Male, Female Artists and the
Track number

=item I<paranoid>

Prompt for all the fields, including Male Artist (1/2), Female Artist
(1/2), Track Number.

=back

=item B<-s>, B<--na>

Simulate or no-act mode: doesn't update the database.

=item B<--upd-only>

Only entries in the database will be updated from the input the user
gives. Make sure you don't use the --no-prompt option along with this
one... else you won't get anything done!

=item B<--upd-song>

Update the tags in the file, ie, ID3 for MP3, comments for Ogg Vorbis.

=item B<--user>=I<xxx>

Username for accessing the database

=item B<--verbose>

Displays some extra information while processing files

=back

=head1 EXAMPLES

=over

=item C<alfilldb --add-only /home/user/tmp/songs/>

This invocation will scan the /home/user/tmp/songs directory
recursively for new songs only. Songs already existing in the database
will not be considered.

=item C<alfilldb --upd-only --prompt=most ~/tmp/songs/>

This invocation will scan the $(HOME)/tmp/songs directory recursively
for the current user for incomplete information in the database.

=item C<alfilldb ~user/tmp/songs/>

This invocation will scan the /home/user/tmp/songs folder for music
files; will add new entries to the database as well as update the
existing ones.

=item C<alfilldb --file=~/tmp/songs/somefile.ogg --file=~/tmp/songs/otherfile.mp3>

This invocation will just add (or update) information about the files
$(HOME)/tmp/songs/somefile.ogg and $(HOME)/tmp/songs/otherfile.mp3.

=back

=head1 CAVEATS

If the B<--prompt> option is not specified, B<alfilldb> will just prompt for
the title of the song being processed. The title information is asked
only if the ID3 tag or the Vorbis comment doesn't contain the
title. This behavior can be overriden by the --no-prompt option, and
in this case, the entry for the file will not be made, since the song
title is a mandatory field for storing song information in the datbase.

If neither of B<--add-only> or B<--upd-only> are specified, the
default action is to add new entries as well as update existing ones.

If the ID3 tags or Vorbis comments for a file were updated after
entries were made in the AudioLink database, they will not be
reflected in the database. If you want to maintain consistency, it is
advised that you keep the database updated (by using the B<--upd-only>
option) and then update the ID3 tag in the MP3 or the comment in the
Ogg Vorbis file (by running B<alfilldb> with the B<--upd-only> option).

=head1 SEE ALSO

=begin man

L<audiolink(1)>, L<alsearch(1)>

=end man

=begin html

<em><a href="audiolink_doc.html">audiolink(1)</a></em>,
<em><a href="alsearch_doc.html">alsearch(1)</a></em>

=end html

The current version of this man page is available on the AudioLink
website at E<lt>http://audiolink.sourceforge.net/E<gt>.

=head1 BUGS

Report bugs related to the AudioLink software or the man pages to the
audiolink-devel mailing list E<lt>audiolink-devel@lists.sourceforge.netE<gt>.

=head1 AUTHOR

This manual page is written and maintained by Amit Shah E<lt>amitshah@gmx.netE<gt>

=head1 COPYRIGHT

The AudioLink package is Copyright (C) 2003, Amit Shah
E<lt>amitshah@gmx.netE<gt>. All the programs and the documentation that come
as part of AudioLink are licensed by the GNU General Public License v2
(GPLv2).

=cut
