source: mp3-find/trunk/lib/MP3/Find/DB.pm @ 10

Last change on this file since 10 was 10, checked in by peter, 18 years ago
  • added test suite for the DB backend
  • doc corrections and updates to Find.pm and Base.pm
  • moved create and update database functions from mp3db to DB.pm
  • added "destroy_db" function to DB.pm
  • documented mp3db
  • set version to 0.02
File size: 7.7 KB
Line 
1package MP3::Find::DB;
2
3use strict;
4use warnings;
5
6use base qw(MP3::Find::Base);
7use Carp;
8
9use DBI;
10use SQL::Abstract;
11
12my $sql = SQL::Abstract->new;
13
14my @COLUMNS = (
15    [ mtime        => 'INTEGER' ],  # the filesystem mtime, so we can do incremental updates
16    [ FILENAME     => 'TEXT' ], 
17    [ TITLE        => 'TEXT' ], 
18    [ ARTIST       => 'TEXT' ], 
19    [ ALBUM        => 'TEXT' ],
20    [ YEAR         => 'INTEGER' ], 
21    [ COMMENT      => 'TEXT' ], 
22    [ GENRE        => 'TEXT' ], 
23    [ TRACKNUM     => 'INTEGER' ], 
24    [ VERSION      => 'NUMERIC' ],
25    [ LAYER        => 'INTEGER' ], 
26    [ STEREO       => 'TEXT' ],
27    [ VBR          => 'TEXT' ],
28    [ BITRATE      => 'INTEGER' ], 
29    [ FREQUENCY    => 'INTEGER' ], 
30    [ SIZE         => 'INTEGER' ], 
31    [ OFFSET       => 'INTEGER' ], 
32    [ SECS         => 'INTEGER' ], 
33    [ MM           => 'INTEGER' ],
34    [ SS           => 'INTEGER' ],
35    [ MS           => 'INTEGER' ], 
36    [ TIME         => 'TEXT' ],
37    [ COPYRIGHT    => 'TEXT' ], 
38    [ PADDING      => 'INTEGER' ], 
39    [ MODE         => 'INTEGER' ],
40    [ FRAMES       => 'INTEGER' ], 
41    [ FRAME_LENGTH => 'INTEGER' ], 
42    [ VBR_SCALE    => 'INTEGER' ],
43);
44
45
46sub search {
47    my $self = shift;
48    my ($query, $dirs, $sort, $options) = @_;
49   
50    croak 'Need a database name to search (set "db_file" in the call to find_mp3s)' unless $$options{db_file};
51   
52    my $dbh = DBI->connect("dbi:SQLite:dbname=$$options{db_file}", '', '', {RaiseError => 1});
53   
54    # use the 'LIKE' operator to ignore case
55    my $op = $$options{ignore_case} ? 'LIKE' : '=';
56   
57    # add the SQL '%' wildcard to match substrings
58    unless ($$options{exact_match}) {
59        for my $value (values %$query) {
60            $value = [ map { "%$_%" } @$value ];
61        }
62    }
63
64    my ($where, @bind) = $sql->where(
65        { map { $_ => { $op => $query->{$_} } } keys %$query },
66        ( @$sort ? [ map { uc } @$sort ] : () ),
67    );
68   
69    my $select = "SELECT * FROM mp3 $where";
70   
71    my $sth = $dbh->prepare($select);
72    $sth->execute(@bind);
73   
74    my @results;
75    while (my $row = $sth->fetchrow_hashref) {
76        push @results, $row;
77    }
78   
79    return @results;
80}
81
82sub create_db {
83    my $self = shift;
84    my $db_file = shift or croak "Need a name for the database I'm about to create";
85    my $dbh = DBI->connect("dbi:SQLite:dbname=$db_file", '', '', {RaiseError => 1});
86    $dbh->do('CREATE TABLE mp3 (' . join(',', map { "$$_[0] $$_[1]" } @COLUMNS) . ')');
87}
88
89sub update_db {
90    my $self = shift;
91    my $db_file = shift or croak "Need the name of the databse to update";
92    my $dirs = shift;
93   
94    my @dirs = ref $dirs eq 'ARRAY' ? @$dirs : ($dirs);
95   
96    my $dbh = DBI->connect("dbi:SQLite:dbname=$db_file", '', '', {RaiseError => 1});
97    my $mtime_sth = $dbh->prepare('SELECT mtime FROM mp3 WHERE FILENAME = ?');
98    my $insert_sth = $dbh->prepare(
99        'INSERT INTO mp3 (' . 
100            join(',', map { $$_[0] } @COLUMNS) .
101        ') VALUES (' .
102            join(',', map { '?' } @COLUMNS) .
103        ')'
104    );
105    my $update_sth = $dbh->prepare(
106        'UPDATE mp3 SET ' . 
107            join(',', map { "$$_[0] = ?" } @COLUMNS) . 
108        ' WHERE FILENAME = ?'
109    );
110   
111    # the number of records added or updated
112    my $count = 0;
113   
114    # look for mp3s using the filesystem backend
115    require MP3::Find::Filesystem;
116    my $finder = MP3::Find::Filesystem->new;
117    for my $mp3 ($finder->find_mp3s(dir => \@dirs, no_format => 1)) {
118        # see if the file has been modified since it was first put into the db
119        $mp3->{mtime} = (stat($mp3->{FILENAME}))[9];
120        $mtime_sth->execute($mp3->{FILENAME});
121        my $records = $mtime_sth->fetchall_arrayref;
122       
123        warn "Multiple records for $$mp3{FILENAME}\n" if @$records > 1;
124       
125        if (@$records == 0) {
126            $insert_sth->execute(map { $mp3->{$$_[0]} } @COLUMNS);
127            print STDERR "A $$mp3{FILENAME}\n";
128            $count++;
129        } elsif ($mp3->{mtime} > $$records[0][0]) {
130            # the mp3 file is newer than its record
131            $update_sth->execute((map { $mp3->{$$_[0]} } @COLUMNS), $mp3->{FILENAME});
132            print STDERR "U $$mp3{FILENAME}\n";
133            $count++;
134        }
135    }
136   
137    # as a workaround for the 'closing dbh with active staement handles warning
138    # (see http://rt.cpan.org/Ticket/Display.html?id=9643#txn-120724)
139    foreach ($mtime_sth, $insert_sth, $update_sth) {
140        $_->{Active} = 1;
141        $_->finish;
142    }
143   
144    return $count;
145}
146
147sub destroy_db {
148    my $self = shift;
149    my $db_file = shift or croak "Need the name of a database to destory";
150    unlink $db_file;
151}
152
153# module return
1541;
155
156=head1 NAME
157
158MP3::Find::DB - SQLite database backend to MP3::Find
159
160=head1 SYNOPSIS
161
162    use MP3::Find::DB;
163    my $finder = MP3::Find::DB->new;
164   
165    my @mp3s = $finder->find_mp3s(
166        dir => '/home/peter/music',
167        query => {
168            artist => 'ilyaimy',
169            album  => 'myxomatosis',
170        },
171        ignore_case => 1,
172        db_file => 'mp3.db',
173    );
174   
175    # you can do things besides just searching the database
176   
177    # create another database
178    $finder->create_db('my_mp3s.db');
179   
180    # update the database from the filesystem
181    $finder->update_db('my_mp3s.db', ['/home/peter/mp3', '/home/peter/cds']);
182   
183    # and then blow it away
184    $finder->destroy_db('my_mp3s.db');
185
186=head1 REQUIRES
187
188L<DBI>, L<DBD::SQLite>, L<SQL::Abstract>
189
190=head1 DESCRIPTION
191
192This is the SQLite database backend for L<MP3::Find>.
193
194B<Note:> I'm still working out some kinks in here, so this backend
195is currently not as stable as the Filesystem backend.
196
197=head2 Special Options
198
199=over
200
201=item C<db_file>
202
203The name of the SQLite database file to use. Defaults to F<~/mp3.db>.
204
205The database should have at least one table named C<mp3> with the
206following schema:
207
208    CREATE TABLE mp3 (
209        mtime         INTEGER,
210        FILENAME      TEXT,
211        TITLE         TEXT,
212        ARTIST        TEXT,
213        ALBUM         TEXT,
214        YEAR          INTEGER,
215        COMMENT       TEXT,
216        GENRE         TEXT,
217        TRACKNUM      INTEGER,
218        VERSION       NUMERIC,
219        LAYER         INTEGER,
220        STEREO        TEXT,
221        VBR           TEXT,
222        BITRATE       INTEGER,
223        FREQUENCY     INTEGER,
224        SIZE          INTEGER,
225        OFFSET        INTEGER,
226        SECS          INTEGER,
227        MM            INTEGER,
228        SS            INTEGER,
229        MS            INTEGER,
230        TIME          TEXT,
231        COPYRIGHT     TEXT,
232        PADDING       INTEGER,
233        MODE          INTEGER,
234        FRAMES        INTEGER,
235        FRAME_LENGTH  INTEGER,
236        VBR_SCALE     INTEGER
237    );
238
239=back
240
241=head1 METHODS
242
243=head2 create_db
244
245    $finder->create_db($db_filename);
246
247Creates a SQLite database in the file named c<$db_filename>.
248
249=head2 update_db
250
251    my $count = $finder->update_db($db_filename, \@dirs);
252
253Searches for all mp3 files in the directories named by C<@dirs>
254using L<MP3::Find::Filesystem>, and adds or updates the ID3 info
255from those files to the database. If a file already has a record
256in the database, then it will only be updated if it has been modified
257sinc ethe last time C<update_db> was run.
258
259=head2 destroy_db
260
261    $finder->destroy_db($db_filename);
262
263Permanantly removes the database.
264
265=head1 TODO
266
267Database maintanence routines (e.g. clear out old entries)
268
269Allow the passing of a DSN or an already created C<$dbh> instead
270of a SQLite database filename.
271
272=head1 SEE ALSO
273
274L<MP3::Find>, L<MP3::Find::Filesystem>, L<mp3db>
275
276=head1 AUTHOR
277
278Peter Eichman <peichman@cpan.org>
279
280=head1 COPYRIGHT AND LICENSE
281
282Copyright (c) 2006 by Peter Eichman. All rights reserved.
283
284This program is free software; you can redistribute it and/or
285modify it under the same terms as Perl itself.
286
287=cut
Note: See TracBrowser for help on using the repository browser.