Index: tags/0.01/Changes
===================================================================
--- tags/0.01/Changes	(revision 4)
+++ tags/0.01/Changes	(revision 4)
@@ -0,0 +1,4 @@
+Revision history for Perl extension MP3::Find.
+
+0.01  29 Jan 2006
+    - first CPAN release
Index: tags/0.01/MANIFEST
===================================================================
--- tags/0.01/MANIFEST	(revision 4)
+++ tags/0.01/MANIFEST	(revision 4)
@@ -0,0 +1,13 @@
+Changes
+Makefile.PL
+MANIFEST
+README
+t/MP3-Find.t
+lib/MP3/Find.pm
+lib/MP3/Find/Base.pm
+lib/MP3/Find/Filesystem.pm
+lib/MP3/Find/DB.pm
+lib/MP3/Find/Util.pm
+bin/mp3find
+bin/mp3db
+META.yml                                 Module meta-data (added by MakeMaker)
Index: tags/0.01/Makefile.PL
===================================================================
--- tags/0.01/Makefile.PL	(revision 4)
+++ tags/0.01/Makefile.PL	(revision 4)
@@ -0,0 +1,18 @@
+use ExtUtils::MakeMaker;
+# See lib/ExtUtils/MakeMaker.pm for details of how to influence
+# the contents of the Makefile that is written.
+WriteMakefile(
+    NAME              => 'MP3::Find',
+    VERSION_FROM      => 'lib/MP3/Find.pm',
+    PREREQ_PM         => { 
+        'MP3::Info'    => 0, 
+        'File::Find'   => 0,
+        'Scalar::Util' => 0,
+    },
+    EXE_FILES => [qw(
+        bin/mp3find
+    )],    
+    ($] >= 5.005 ?     ## Add these new keywords supported since 5.005
+      (ABSTRACT_FROM  => 'lib/MP3/Find.pm', # retrieve abstract from module
+       AUTHOR         => 'Peter Eichman <peichamn@cpan.org>') : ()),
+);
Index: tags/0.01/README
===================================================================
--- tags/0.01/README	(revision 4)
+++ tags/0.01/README	(revision 4)
@@ -0,0 +1,43 @@
+MP3::Find version 0.01
+======================
+DESCRIPTION
+    This module allows you to search for MP3 files by their ID3 tags. You
+    can ask for the results to be sorted by one or more of those tags, and
+    return either the list of filenames (the deault), a "printf"-style
+    formatted string for each file using its ID3 tags, or the actual Perl
+    data structure representing the results.
+
+INSTALL
+    To install this module type the following:
+
+        perl Makefile.PL
+        make
+        make test
+        make install
+
+SYNOPSIS
+        use MP3Find;
+    
+        print "$_\n" foreach find_mp3s(
+            dir => '/home/peter/cds',
+            query => {
+                artist => 'ilyaimy',
+                title => 'deep in the am',
+            },
+            ignore_case => 1,
+            match_words => 1,
+            sort => [qw(year album tracknum)],
+            printf => '%2n. %a - %t (%b: %y)',
+        );
+
+REQUIRES
+    File::Find, MP3::Info, Scalar::Util
+
+    DBI and DBD::SQLite are needed if you want to have a database backend.
+
+COPYRIGHT AND LICENSE
+    Copyright (c) 2006 by Peter Eichman. All rights reserved.
+
+    This program is free software; you can redistribute it and/or modify it
+    under the same terms as Perl itself.
+
Index: tags/0.01/bin/cql
===================================================================
--- tags/0.01/bin/cql	(revision 4)
+++ tags/0.01/bin/cql	(revision 4)
@@ -0,0 +1,30 @@
+#!/usr/bin/perl -w
+use strict;
+
+package MyVisitor;
+use base qw( CQL::Visitor );
+
+sub term {
+    my ($self,$node) = @_;
+    # do something to the node
+    print $node->{qualifier}, " ", $node->{term}, "\n";
+}
+
+package main;
+
+use CQL::Parser;
+use YAML;
+
+my $p = CQL::Parser->new;
+
+my $cql = <<END;
+artist = (ilyaimy or rob) and title = angel
+END
+
+my $root = $p->parse($cql);
+
+#print Dump($root);
+
+my $visitor = MyVisitor->new;
+$visitor->visit($root);
+
Index: tags/0.01/bin/mp3db
===================================================================
--- tags/0.01/bin/mp3db	(revision 4)
+++ tags/0.01/bin/mp3db	(revision 4)
@@ -0,0 +1,148 @@
+#!/usr/bin/perl -w
+use strict;
+
+use lib '/home/peter/projects/mp3-find/lib';
+use DBI;
+use MP3::Find;
+
+use File::Spec::Functions qw(catfile);
+use Getopt::Long;
+GetOptions(
+    'create' => \my $CREATE,
+    'file|f=s' => \my $DB_FILE,
+);
+
+$DB_FILE ||= catfile($ENV{HOME}, 'mp3.db');
+
+#TODO: hints on numeric columns
+my @COLUMNS = (
+    [ mtime => 'INTEGER' ],
+    [ FILENAME => 'TEXT' ], 
+    [ TITLE => 'TEXT' ], 
+    [ ARTIST => 'TEXT' ], 
+    [ ALBUM => 'TEXT' ],
+    [ YEAR => 'INTEGER' ], 
+    [ COMMENT => 'TEXT' ], 
+    [ GENRE => 'TEXT' ], 
+    [ TRACKNUM => 'INTEGER' ], 
+    [ VERSION => 'NUMERIC' ],
+    [ LAYER => 'INTEGER' ], 
+    [ STEREO => 'TEXT' ],
+    [ VBR => 'TEXT' ],
+    [ BITRATE => 'INTEGER' ], 
+    [ FREQUENCY => 'INTEGER' ], 
+    [ SIZE => 'INTEGER' ], 
+    [ OFFSET => 'INTEGER' ], 
+    [ SECS => 'INTEGER' ], 
+    [ MM => 'INTEGER' ],
+    [ SS => 'INTEGER' ],
+    [ MS => 'INTEGER' ], 
+    [ TIME => 'TEXT' ],
+    [ COPYRIGHT => 'TEXT' ], 
+    [ PADDING => 'INTEGER' ], 
+    [ MODE => 'INTEGER' ],
+    [ FRAMES => 'INTEGER' ], 
+    [ FRAME_LENGTH => 'INTEGER' ], 
+    [ VBR_SCALE => 'INTEGER' ],
+);
+
+my @DIRS = @ARGV;
+push @DIRS, $ENV{HOME} unless @DIRS;
+
+
+my $dbh = DBI->connect("dbi:SQLite:dbname=$DB_FILE",'','', { RaiseError => 1 });
+
+create_table($dbh) if $CREATE;
+read_mp3s(\@DIRS);
+
+sub read_mp3s {
+    my $dirs = shift;
+
+    my $mtime_sth = $dbh->prepare('SELECT mtime FROM mp3 WHERE FILENAME = ?');
+    my $insert_sth = $dbh->prepare(
+        'INSERT INTO mp3 (' . 
+            join(',', map { $$_[0] } @COLUMNS) .
+        ') VALUES (' .
+            join(',', map { '?' } @COLUMNS) .
+        ')'
+    );
+    my $update_sth = $dbh->prepare(
+        'UPDATE mp3 SET ' . 
+            join(',', map { "$$_[0] = ?" } @COLUMNS) . 
+        ' WHERE FILENAME = ?'
+    );
+    
+    for my $mp3 (find_mp3s(dir => $dirs, no_format => 1)) {
+        # see if the file has been modified since it was first put into the db
+        $mp3->{mtime} = (stat($mp3->{FILENAME}))[9];
+        $mtime_sth->execute($mp3->{FILENAME});
+        my $records = $mtime_sth->fetchall_arrayref;
+        
+        warn "Multiple records for $$mp3{FILENAME}\n" if @$records > 1;
+        
+        if (@$records == 0) {
+            $insert_sth->execute(map { $mp3->{$$_[0]} } @COLUMNS);
+            print "A $$mp3{FILENAME}\n";
+        } elsif ($mp3->{mtime} > $$records[0][0]) {
+            # the mp3 file is newer than its record
+            $update_sth->execute((map { $mp3->{$$_[0]} } @COLUMNS), $mp3->{FILENAME});
+            print "U $$mp3{FILENAME}\n";
+        }
+    }
+    
+    # as a workaround for the 'closing dbh with active staement handles warning
+    # (see http://rt.cpan.org/Ticket/Display.html?id=9643#txn-120724)
+    # NOT WORKING!!!
+    foreach ($mtime_sth, $insert_sth, $update_sth) {
+        $_->{Active} = 1;
+        $_->finish;
+    }
+}
+
+#TODO: hints on numeric vs. string columns, for proper sorting
+sub create_table {
+    my $dbh = shift;
+    $dbh->do('CREATE TABLE mp3 (' . join(',', map { "$$_[0] $$_[1]" } @COLUMNS) . ')');
+}
+
+=begin
+
+    CREATE TABLE mp3 (
+        mtime,
+        
+        FILENAME,
+        
+        TITLE,
+        ARTIST,
+        ALBUM,
+        YEAR,
+        COMMENT,
+        GENRE,
+        TRACKNUM,
+        
+        VERSION,         -- MPEG audio version (1, 2, 2.5)
+        LAYER,           -- MPEG layer description (1, 2, 3)
+        STEREO,          -- boolean for audio is in stereo
+    
+        VBR,             -- boolean for variable bitrate
+        BITRATE,         -- bitrate in kbps (average for VBR files)
+        FREQUENCY,       -- frequency in kHz
+        SIZE,            -- bytes in audio stream
+        OFFSET,          -- bytes offset that stream begins
+    
+        SECS,            -- total seconds
+        MM,              -- minutes
+        SS,              -- leftover seconds
+        MS,              -- leftover milliseconds
+        TIME,            -- time in MM:SS
+    
+        COPYRIGHT,       -- boolean for audio is copyrighted
+        PADDING,         -- boolean for MP3 frames are padded
+        MODE,            -- channel mode (0 = stereo, 1 = joint stereo,
+                         -- 2 = dual channel, 3 = single channel)
+        FRAMES,          -- approximate number of frames
+        FRAME_LENGTH,    -- approximate length of a frame
+        VBR_SCALE        -- VBR scale from VBR header
+    );
+
+=cut
Index: tags/0.01/bin/mp3find
===================================================================
--- tags/0.01/bin/mp3find	(revision 4)
+++ tags/0.01/bin/mp3find	(revision 4)
@@ -0,0 +1,115 @@
+#!/usr/bin/perl -w
+use strict;
+
+use Getopt::Long qw(:config pass_through); # use pass_through so we can get the query args
+use lib '/home/peter/projects/mp3-find/lib';
+
+use MP3::Find qw(Filesystem);
+use MP3::Find::Util qw(build_query);
+use File::Spec::Functions qw(catfile);
+
+GetOptions(
+    'ignore-case|i' => \my $IGNORE_CASE,
+    'exact-match|w' => \my $EXACT_MATCH,
+    'sort|s=s'      => \my $SORT_TAG,
+    'printf=s'      => \my $FORMAT,
+);
+
+my ($DIRS, $QUERY) = build_query(@ARGV);
+push @$DIRS, '.' unless @$DIRS;
+
+print "$_\n" foreach find_mp3s(
+    dir         => $DIRS,
+    query       => $QUERY,
+    ignore_case => $IGNORE_CASE,
+    exact_match => $EXACT_MATCH,
+    ($SORT_TAG ? (sort => [split(/,/, $SORT_TAG)]) : ()),
+    printf      => $FORMAT,
+    db_file     => catfile($ENV{HOME}, 'mp3.db'),
+);
+
+=head1 NAME
+
+mp3find - Find MP3 files based on their ID3 tags or info
+
+=head1 SYNOPSIS
+
+    $ mp3find ~/cds -i -artist beatles -sort year,album,tracknum -printf '%2n. %a - %t (%b: %y)'
+     1. The Beatles - Magical Mystery Tour (Magical Mystery Tour: 1967)
+     2. The Beatles - The Fool on the Hill (Magical Mystery Tour: 1967)
+     3. The Beatles - Flying (Magical Mystery Tour: 1967)
+     4. The Beatles - Blue Jay Way (Magical Mystery Tour: 1967)
+     5. The Beatles - Your Mother Should Know (Magical Mystery Tour: 1967)
+     6. The Beatles - I Am The Walrus (Magical Mystery Tour: 1967)
+    # etc.
+    
+    # shuffle and play your entire mp3 collection
+    $ mp3find | xargs madplay -z
+    
+    # ...or just your Sabbath
+    $ mp3find -i -artist 'black sabbath' | xargs madplay -z
+
+=head1 DESCRIPTION
+
+    $ mp3find [options] [directory] [<-field> <pattern> [<-field> <pattern> ...]]
+
+The real guts of the operation are in L<MP3::Find>.
+
+=head2 OPTIONS
+
+=over
+
+=item C<-ignore-case>, C<-i>
+
+Case insensitive matching.
+
+=item C<-exact-match>, C<-w>
+
+All search patterns must match the entire value, and not just a
+substring. This has the same effect as putting a C<^> and C<$>
+around each pattern.
+
+=item C<-sort>
+
+Which ID3 fields to sort the results by; separate multiple fields
+with commas. The default behavior just returns the filenames in the
+order that L<File::Find> finds them.
+
+=item C<-printf>
+
+The output format for each file found. The available format codes are:
+
+    %a - artist
+    %t - title
+    %b - album
+    %n - track number
+    %y - year
+    %g - genre
+    %% - literal '%'
+
+Numeric modifiers may be used; they are interpreted like modifiers to
+the C<%s> code in Perl's C<printf> function.
+
+If no C<-printf> option is used, the full path to the file is printed
+instead.
+
+=item C<< -<field> <pattern> [patterns...] >>
+
+The fields you are searching on. More than one pattern for a given field
+are combined with 'OR', while the fields to be matched are 'AND'-ed together.
+For the list of recognized fields, see L<MP3::Find>.
+
+=back
+
+=head1 AUTHOR
+
+Peter Eichman <peichman@cpan.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (c) 2006 by Peter Eichman. All rights reserved.
+
+This program is free software; you can redistribute it and/or
+modify it under the same terms as Perl itself.
+
+=cut
Index: tags/0.01/bin/tagger
===================================================================
--- tags/0.01/bin/tagger	(revision 4)
+++ tags/0.01/bin/tagger	(revision 4)
@@ -0,0 +1,30 @@
+#!/usr/bin/perl -w
+use strict;
+
+use lib '/home/peter/projects/mp3-find/lib';
+use MP3::Find qw(Filesystem);
+use MP3::Find::Util qw(build_query);
+use File::Spec::Functions qw(catfile);
+
+use Term::ReadKey;
+
+my ($DIRS, $QUERY) = build_query(@ARGV);
+push @$DIRS, '.' unless @$DIRS;
+
+for my $mp3 (find_mp3s(
+    dir         => $DIRS,
+    query       => $QUERY,
+    ignore_case => 1, #$IGNORE_CASE,
+    exact_match => 0, #$EXACT_MATCH,
+    no_format => 1,
+    db_file     => catfile($ENV{HOME}, 'mp3.db'),
+)) {
+    print "$$mp3{FILENAME}\n";
+    my $i;
+    for (qw(ARTIST TITLE ALBUM YEAR GENRE)) {
+        printf "[%d] %-6s  %s\n", ++$i, $_, $$mp3{$_};
+    }
+    print "Change [1-5]? ";
+    my $key = ReadKey(0);
+}   
+
Index: tags/0.01/lib/MP3/Find.pm
===================================================================
--- tags/0.01/lib/MP3/Find.pm	(revision 4)
+++ tags/0.01/lib/MP3/Find.pm	(revision 4)
@@ -0,0 +1,195 @@
+package MP3::Find;
+
+use strict;
+use warnings;
+
+use base qw(Exporter);
+use vars qw($VERSION @EXPORT);
+
+use Carp;
+
+$VERSION = '0.01';
+
+@EXPORT = qw(find_mp3s);
+
+my $finder;
+sub import {
+    my $calling_pkg = shift;
+    # default to a filesystem search
+    my $finder_type = shift || 'Filesystem';
+    my $package = "MP3::Find::$finder_type";
+    eval "require $package";
+    croak $@ if $@;
+    $finder = $package->new;
+    __PACKAGE__->export_to_level(1, @EXPORT);
+}
+
+sub find_mp3s { $finder->find_mp3s(@_) }
+
+
+# module return
+1;
+
+=head1 NAME
+
+MP3::Find - Search and sort MP3 files based on their ID3 tags
+
+=head1 SYNOPSIS
+
+    # select with backend you want
+    use MP3::Find qw(Filesystem);
+    
+    print "$_\n" foreach find_mp3s(
+        dir => '/home/peter/cds',
+        query => {
+            artist => 'ilyaimy',
+            title => 'deep in the am',
+        },
+        ignore_case => 1,
+        match_words => 1,
+        sort => [qw(year album tracknum)],
+        printf => '%2n. %a - %t (%b: %y)',
+    );
+
+=head1 DESCRIPTION
+
+This module allows you to search for MP3 files by their ID3 tags.
+You can ask for the results to be sorted by one or more of those
+tags, and return either the list of filenames (the deault), a
+C<printf>-style formatted string for each file using its ID3 tags,
+or the actual Perl data structure representing the results.
+
+There are currently two backends to this module: L<MP3::Find::Filesystem>
+and L<MP3::Find::DB>. You choose which one you want by passing its
+name as the argument to you C<use> statement; B<MP3::Find> will look for
+a B<MP3::Find::$BACKEND> module. If no backend name is given, it will
+default to using L<MP3::Find::Filesystem>.
+
+B<Note:> I'm still working out some kinks in the DB backend, so it
+is currently not as stable as the Filesystem backend.
+
+=head1 REQUIRES
+
+L<File::Find>, L<MP3::Info>, and L<Scalar::Util> are needed for
+the filesystem backend (L<MP3::Find::Filesystem>).
+
+L<DBI>, L<DBD::SQLite>, and L<SQL::Abstract> are needed for the
+database backend (L<MP3::Find::DB>).
+
+=head1 EXPORTS
+
+=head2 find_mp3s
+
+    my @results = find_mp3s(%options);
+
+Takes the following options:
+
+=over
+
+=item C<dir>
+
+Where to start the search. This can either be a single string or
+an arrayref. Defaults to your home directory.
+
+=item C<query>
+
+Hashref of search parameters. Recognized fields are anything that
+L<MP3::Info> knows about. Field names can be given in either upper
+or lower case; C<find_mp3s> will convert them into upper case for 
+you. Value may either be strings, which are converted into regular
+exporessions, or may be C<qr[...]> regular expressions already.
+
+=item C<ignore_case>
+
+Ignore case when matching search strings to the ID3 tag values.
+
+=item C<exact_match>
+
+Adds an implicit C<^> and C<$> around each query string. Does nothing
+if the query is already a regular expression.
+
+=item C<sort>
+
+What field or fields to sort the results by. Can either be a single
+scalar field name to sort by, or an arrayref of field names. Again,
+acceptable field names are anything that L<MP3::Info> knows about;
+field names will be converted to upper case as with the C<query>
+option.
+
+=item C<printf>
+
+By default, C<find_mp3s> just returns the list of filenames. The 
+C<printf> option allows you to provide a formatting string to apply
+to the data for each file. The style is roughly similar to Perl's
+C<printf> format strings. The following formatting codes are 
+recognized:
+
+    %a - artist
+    %t - title
+    %b - album
+    %n - track number
+    %y - year
+    %g - genre
+    %% - literal '%'
+
+Numeric modifers may be used in the same manner as with C<%s> in
+Perl's C<printf>.
+
+=item C<no_format>
+
+Causes C<find_mp3s> to return an array of hashrefs instead of an array
+of (formatted) strings. Each hashref consists of the key-value pairs
+from C<MP3::Info::get_mp3_tag> and C<MP3::Info::get_mp3_info>, plus
+the key C<FILENAME> (with the obvious value ;-)
+
+    @results = (
+        {
+            FILENAME => ...,
+            TITLE    => ...,
+            ARTIST   => ...,
+            ...
+            SECS     => ...,
+            BITRATE  => ...,
+            ...
+        },
+        ...
+    );
+
+=back
+
+=head1 BUGS
+
+There are probably some in there; let me know if you find any (patches
+welcome).
+
+=head1 TODO
+
+Better tests, using some actual sample mp3 files.
+
+Other backends (a caching filesystem backend, perhaps?)
+
+=head1 SEE ALSO
+
+L<MP3::Find::Filesystem>, L<MP3::Find::DB>
+
+L<mp3find> is the command line frontend to this module (it
+currently only uses the filesystem backend).
+
+See L<MP3::Info> for more information about the fields you can
+search and sort on.
+
+L<File::Find::Rule::MP3Info> is another way to search for MP3
+files based on their ID3 tags.
+
+=head1 AUTHOR
+
+Peter Eichman <peichman@cpan.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (c) 2006 by Peter Eichman. All rights reserved.
+
+This program is free software; you can redistribute it and/or
+modify it under the same terms as Perl itself.
+
+=cut
Index: tags/0.01/lib/MP3/Find/Base.pm
===================================================================
--- tags/0.01/lib/MP3/Find/Base.pm	(revision 4)
+++ tags/0.01/lib/MP3/Find/Base.pm	(revision 4)
@@ -0,0 +1,206 @@
+package MP3::Find::Base;
+
+use strict;
+use warnings;
+
+use vars qw($VERSION);
+use Carp;
+
+$VERSION = '0.01';
+
+my %format_codes = (
+    a => 'ARTIST',
+    t => 'TITLE',
+    b => 'ALBUM',
+    n => 'TRACKNUM',
+    y => 'YEAR',
+    g => 'GENRE',
+);
+
+sub new {
+    my $invocant = shift;
+    my $class = ref $invocant || $invocant;
+    my %options = @_;
+    my $self = \%options;
+    bless $self, $class;
+}
+
+sub find_mp3s {
+    my $self = shift;
+    my %opt = @_;
+    
+    my $dir = $opt{dir} || $ENV{HOME};
+    my @DIRS = ref $dir eq 'ARRAY' ? @$dir : ($dir);
+    
+    my %QUERY = %{ $opt{query} || {} };
+    
+    # array ref for multiple sort fields, but allow
+    # a simple scalar for single values
+    my @SORT = $opt{sort} ? 
+        (ref $opt{sort} eq 'ARRAY' ? @{ $opt{sort} } : ($opt{sort})) :
+        ();
+    
+    foreach (keys %QUERY) {
+        # so we don't have spurious warnings when trying to match against undef
+        delete $QUERY{$_} unless defined $QUERY{$_};
+        # package everything uniformly, so subclasses don't need to unpack it
+        $QUERY{$_} = [ $QUERY{$_} ] unless ref $QUERY{$_} eq 'ARRAY';
+    }
+    
+    # do the search
+    my @results = $self->search(\%QUERY, \@DIRS, \@SORT, \%opt);
+    
+    # maybe they want the unformatted data
+    return @results if $opt{no_format};
+    
+    if ($opt{printf}) {
+        # printf style output format
+        foreach (@results) {
+            my $output = $opt{printf};
+            for my $code (keys %format_codes) {
+                
+                while ($output =~ m/%((-\d)?\d*)$code/g) {
+                    # field size modifier
+                    my $modifier = $1 || '';
+                    # figure out the size of the formating code
+                    my $code_size = 2 + length($modifier);
+                    my $value = sprintf("%${modifier}s", $_->{$format_codes{$code}} || '');
+                    substr($output, pos($output) - $code_size, $code_size, $value);
+                }
+            }
+            # to allow literal '%'
+            $output =~ s/%%/%/g;        
+            $_ = $output;
+        }
+    } else {
+        # just the filenames, please
+        @results = map { $_->{FILENAME} } @results;
+    }
+    
+    return @results;
+}
+
+sub search {
+    croak "Method 'search' not implemented in " . __PACKAGE__;
+}
+
+# module return
+1;
+
+=head1 NAME
+
+MP3::Find::Base - Base class for MP3::Find finders
+
+=head1 SYNOPSIS
+
+    package MyFinder;
+    use base 'MP3::Find::Base';
+    
+    sub search {
+        my $self = shift;
+        my ($query, $dirs, $sort, $options) = @_;
+        
+        # do something to find and sort the mp3s...
+        my @results = do_something(...);
+        
+        return @results;
+    }
+    
+    package main;
+    my $finder = MyFinder->new;
+    
+    # see MP3::Find for details about %options
+    print "$_\n" foreach $finder->find_mp3s(\%options);        
+
+=head1 DESCRIPTION
+
+This is the base class for the classes that actually do the
+searching and sorting for L<MP3::Find>.
+
+=head1 METHODS
+
+=head2 new
+
+Really simple constructor. If you pass it a hash of options, it
+will hang on to them for you.
+
+=head2 search
+
+This is the one you should override in your subclass. If you
+don't, the base class C<search> method will croak.
+
+The C<search> method is called by the C<find_mp3s> method with
+the following arguments: the finder object, a hashref of query
+parameters, an arrayref of directories to search, and a hashref
+of miscellaneous options.
+
+The search method should return a list of hashrefs representing
+the results of the search. Each hashref should have the following
+keys (all except C<FILENAME> are derived from the keys returned
+by the C<get_mp3tag> and C<get_mp3Info> functions from L<MP3::Info>):
+
+    FILENAME
+    
+    TITLE
+    ARTIST
+    ALBUM
+    YEAR
+    COMMENT
+    GENRE
+    TRACKNUM
+    
+    VERSION         -- MPEG audio version (1, 2, 2.5)
+    LAYER           -- MPEG layer description (1, 2, 3)
+    STEREO          -- boolean for audio is in stereo
+    
+    VBR             -- boolean for variable bitrate
+    BITRATE         -- bitrate in kbps (average for VBR files)
+    FREQUENCY       -- frequency in kHz
+    SIZE            -- bytes in audio stream
+    OFFSET          -- bytes offset that stream begins
+    
+    SECS            -- total seconds
+    MM              -- minutes
+    SS              -- leftover seconds
+    MS              -- leftover milliseconds
+    TIME            -- time in MM:SS
+    
+    COPYRIGHT       -- boolean for audio is copyrighted
+    PADDING         -- boolean for MP3 frames are padded
+    MODE            -- channel mode (0 = stereo, 1 = joint stereo,
+                    -- 2 = dual channel, 3 = single channel)
+    FRAMES          -- approximate number of frames
+    FRAME_LENGTH    -- approximate length of a frame
+    VBR_SCALE       -- VBR scale from VBR header
+
+
+=head2 find_mp3s
+
+The method that should be called by the program doing the searching.
+
+See L<MP3::Find> for an explanation of the options that can be passed
+to C<find_mp3s>.
+
+=head1 TODO
+
+More format codes? Possibly look into using L<String::Format>
+
+=head1 SEE ALSO
+
+L<MP3::Find>, L<MP3::Find::Filesystem>, L<MP3::Find::DB>
+
+See L<MP3::Info> for more information about the fields you can
+search and sort on.
+
+=head1 AUTHOR
+
+Peter Eichman <peichman@cpan.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (c) 2006 by Peter Eichman. All rights reserved.
+
+This program is free software; you can redistribute it and/or
+modify it under the same terms as Perl itself.
+
+=cut
Index: tags/0.01/lib/MP3/Find/DB.pm
===================================================================
--- tags/0.01/lib/MP3/Find/DB.pm	(revision 4)
+++ tags/0.01/lib/MP3/Find/DB.pm	(revision 4)
@@ -0,0 +1,145 @@
+package MP3::Find::DB;
+
+use strict;
+use warnings;
+
+use base qw(MP3::Find::Base);
+
+use DBI;
+use SQL::Abstract;
+
+my $sql = SQL::Abstract->new;
+
+sub search {
+    my $self = shift;
+    my ($query, $dirs, $sort, $options) = @_;
+    
+    my $dbh = DBI->connect("dbi:SQLite:dbname=$$options{db_file}", '', '');
+    
+    # use the 'LIKE' operator to ignore case
+    my $op = $$options{ignore_case} ? 'LIKE' : '=';
+    
+    # add the SQL '%' wildcard to match substrings
+    unless ($$options{exact_match}) {
+        for my $value (values %$query) {
+            $value = [ map { "%$_%" } @$value ];
+        }
+    }
+
+    my ($where, @bind) = $sql->where(
+        { map { $_ => { $op => $query->{$_} } } keys %$query },
+        ( @$sort ? [ map { uc } @$sort ] : () ),
+    );
+    
+    my $select = "SELECT * FROM mp3 $where";
+    
+    my $sth = $dbh->prepare($select);
+    $sth->execute(@bind);
+    
+    my @results;
+    while (my $row = $sth->fetchrow_hashref) {
+        push @results, $row;
+    }
+    
+    return @results;
+}
+
+# module return
+1;
+
+=head1 NAME
+
+MP3::Find::DB - SQLite database backend to MP3::Find
+
+=head1 SYNOPSIS
+
+    use MP3::Find::DB;
+    my $finder = MP3::Find::DB->new;
+    
+    my @mp3s = $finder->find_mp3s(
+        dir => '/home/peter/music',
+        query => {
+            artist => 'ilyaimy',
+            album  => 'myxomatosis',
+        },
+        ignore_case => 1,
+    );
+
+=head1 REQUIRES
+
+L<DBI>, L<DBD::SQLite>, L<SQL::Abstract>
+
+=head1 DESCRIPTION
+
+This is the SQLite database backend for L<MP3::Find>.
+
+B<Note:> I'm still working out some kinks in here, so this backend
+is currently not as stable as the Filesystem backend.
+
+=head2 Special Options
+
+=over
+
+=item C<db_file>
+
+The name of the SQLite database file to use. Defaults to F<~/mp3.db>.
+
+The database should have at least one table named C<mp3> with the
+following schema:
+
+    CREATE TABLE mp3 (
+        mtime         INTEGER,
+        FILENAME      TEXT, 
+        TITLE         TEXT, 
+        ARTIST        TEXT, 
+        ALBUM         TEXT,
+        YEAR          INTEGER, 
+        COMMENT       TEXT, 
+        GENRE         TEXT, 
+        TRACKNUM      INTEGER, 
+        VERSION       NUMERIC,
+        LAYER         INTEGER, 
+        STEREO        TEXT,
+        VBR           TEXT,
+        BITRATE       INTEGER, 
+        FREQUENCY     INTEGER, 
+        SIZE          INTEGER, 
+        OFFSET        INTEGER, 
+        SECS          INTEGER, 
+        MM            INTEGER,
+        SS            INTEGER,
+        MS            INTEGER, 
+        TIME          TEXT,
+        COPYRIGHT     TEXT, 
+        PADDING       INTEGER, 
+        MODE          INTEGER,
+        FRAMES        INTEGER, 
+        FRAME_LENGTH  INTEGER, 
+        VBR_SCALE     INTEGER
+    );
+
+=back
+
+=head1 TODO
+
+Move the database/table creation code from F<mp3db> into this
+module.
+
+Database maintanence routines (e.g. clear out old entries)
+
+=head1 SEE ALSO
+
+L<MP3::Find>, L<MP3::Find::DB>
+
+=head1 AUTHOR
+
+Peter Eichman <peichman@cpan.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (c) 2006 by Peter Eichman. All rights reserved.
+
+This program is free software; you can redistribute it and/or
+modify it under the same terms as Perl itself.
+
+=cut
Index: tags/0.01/lib/MP3/Find/Filesystem.pm
===================================================================
--- tags/0.01/lib/MP3/Find/Filesystem.pm	(revision 4)
+++ tags/0.01/lib/MP3/Find/Filesystem.pm	(revision 4)
@@ -0,0 +1,123 @@
+package MP3::Find::Filesystem;
+
+use strict;
+use warnings;
+
+use base 'MP3::Find::Base';
+
+use File::Find;
+use MP3::Info;
+use Scalar::Util qw(looks_like_number);
+
+use_winamp_genres();
+
+sub search {
+    my $self = shift;
+    my ($query, $dirs, $sort, $options) = @_;
+    
+    # prep the search patterns as regexes
+    foreach (keys(%$query)) {
+        my $ref = ref $$query{$_};
+        # make arrays into 'OR' searches
+        if ($ref eq 'ARRAY') {
+            $$query{$_} = '(' . join('|', @{ $$query{$_} }) . ')';
+        }
+        # convert to a regex unless it already IS a regex        
+        unless ($ref eq 'Regexp') {
+            $$query{$_} = "^$$query{$_}\$" if $$options{exact_match};
+            $$query{$_} = $$options{ignore_case} ? qr[$$query{$_}]i : qr[$$query{$_}];
+        }
+    }
+    
+    # run the actual find
+    my @results;
+    find(sub { match_mp3($File::Find::name, $query, \@results) }, $_) foreach @$dirs;
+    
+    # sort the results
+    if (@$sort) {
+        @results = sort {
+            my $compare;
+            foreach (map { uc } @$sort) {
+                # use Scalar::Util to do the right sort of comparison
+                $compare = (looks_like_number($a->{$_}) && looks_like_number($b->{$_})) ?
+                    $a->{$_} <=> $b->{$_} :
+                    $a->{$_} cmp $b->{$_};
+                # we found a field they differ on
+                last if $compare;
+            }
+            return $compare;
+        } @results;
+    }
+    
+    return @results
+}
+
+sub match_mp3 {
+    my ($filename, $query, $results) = @_;
+    
+    return unless $filename =~ m{[^/]\.mp3$};
+    my $mp3 = {
+        FILENAME => $filename,
+        %{ get_mp3tag($filename)  || {} },
+        %{ get_mp3info($filename) || {} },
+    };
+    for my $field (keys(%{ $query })) {
+        my $value = $mp3->{uc($field)};
+        return unless defined $value;
+        return unless $value =~ $query->{$field};
+    }
+    
+    push @{ $results }, $mp3;
+}
+
+# module return
+1;
+
+=head1 NAME
+
+MP3::Find::Filesystem - File::Find-based backend to MP3::Find
+
+=head1 SYNOPSIS
+
+    use MP3::Find::Filesystem;
+    my $finder = MP3::Find::Filesystem->new;
+    
+    my @mp3s = $finder->find_mp3s(
+        dir => '/home/peter/music',
+        query => {
+            artist => 'ilyaimy',
+            album  => 'myxomatosis',
+        },
+        ignore_case => 1,
+    );
+
+=head1 REQUIRES
+
+L<File::Find>, L<MP3::Info>, L<Scalar::Util>
+
+=head1 DESCRIPTION
+
+This module implements the C<search> method from L<MP3::Find::Base>
+using a L<File::Find> based search of the local filesystem.
+
+=head2 Special Options
+
+There are no special options for B<MP3::Find::Filesystem>. See
+L<MP3::Find> for the description of the general options.
+
+=head1 SEE ALSO
+
+L<MP3::Find>, L<MP3::Find::DB>
+
+=head1 AUTHOR
+
+Peter Eichman <peichman@cpan.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (c) 2006 by Peter Eichman. All rights reserved.
+
+This program is free software; you can redistribute it and/or
+modify it under the same terms as Perl itself.
+
+=cut
Index: tags/0.01/lib/MP3/Find/Util.pm
===================================================================
--- tags/0.01/lib/MP3/Find/Util.pm	(revision 4)
+++ tags/0.01/lib/MP3/Find/Util.pm	(revision 4)
@@ -0,0 +1,41 @@
+package MP3::Find::Util;
+
+use strict;
+use warnings;
+
+use base qw(Exporter);
+use vars qw(@EXPORT_OK);
+
+@EXPORT_OK = qw(build_query);
+
+sub build_query {
+    my @args = @_;
+    
+    # first find all the directories
+    my @dirs;
+    while (local $_ = shift @args) {
+        if (/^-/) {
+            # whoops, there's the beginning of the query
+            unshift @args, $_;
+            last;
+        } else {
+            push @dirs, $_;
+        }
+    }
+    
+    # now build the query hash
+    my %query;
+    my $field;
+    while (local $_ = shift @args) {
+        if (/^--?(.*)/) {
+            $field = uc $1;
+        } else {
+            $field ? push @{ $query{$field} }, $_ : die "Need a field name before value '$_'\n";
+        }
+    }
+    
+    return (\@dirs, \%query);
+}
+
+# module return
+1;
Index: tags/0.01/mkreadme
===================================================================
--- tags/0.01/mkreadme	(revision 4)
+++ tags/0.01/mkreadme	(revision 4)
@@ -0,0 +1,17 @@
+#!/bin/sh
+perl -I./lib -M$1 -e"\$ver = '$1 version ' . $1->VERSION; print qq[\$ver\\n] . '=' x length(\$ver) . qq[\\n]"
+
+PM_FILE=./lib/`echo $1 | sed 's/::/\//g'`.pm
+
+podselect -section 'DESCRIPTION/!.' $PM_FILE | pod2text
+
+echo 'INSTALL'
+echo '    To install this module type the following:'
+echo
+echo '        perl Makefile.PL'
+echo '        make'
+echo '        make test'
+echo '        make install'
+echo
+
+podselect -section 'SYNOPSIS|REQUIRES|COPYRIGHT AND LICENSE' $PM_FILE | pod2text
Index: tags/0.01/t/MP3-Find.t
===================================================================
--- tags/0.01/t/MP3-Find.t	(revision 4)
+++ tags/0.01/t/MP3-Find.t	(revision 4)
@@ -0,0 +1,18 @@
+#!/usr/bin/perl -w
+use strict;
+
+# Before `make install' is performed this script should be runnable with
+# `make test'. After `make install' it should work as `perl MP3-Find.t'
+
+#########################
+
+# change 'tests => 1' to 'tests => last_test_to_print';
+
+use Test::More tests => 1;
+BEGIN { use_ok('MP3::Find') };
+
+#########################
+
+# Insert your test code below, the Test::More module is use()ed here so read
+# its man page ( perldoc Test::More ) for help writing this test script.
+
