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

Last change on this file since 13 was 13, checked in by peter, 18 years ago
  • added mp3db to EXE_FILES in Makefile.PL
  • added "sync_db" method to DB.pm; removes outdated records from the database
  • increased version number to '0.03' in Find.pm
File size: 8.8 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        #TODO: maybe print status updates somewhere else?
126        if (@$records == 0) {
127            $insert_sth->execute(map { $mp3->{$$_[0]} } @COLUMNS);
128            print STDERR "A $$mp3{FILENAME}\n";
129            $count++;
130        } elsif ($mp3->{mtime} > $$records[0][0]) {
131            # the mp3 file is newer than its record
132            $update_sth->execute((map { $mp3->{$$_[0]} } @COLUMNS), $mp3->{FILENAME});
133            print STDERR "U $$mp3{FILENAME}\n";
134            $count++;
135        }
136    }
137   
138    # as a workaround for the 'closing dbh with active staement handles warning
139    # (see http://rt.cpan.org/Ticket/Display.html?id=9643#txn-120724)
140    foreach ($mtime_sth, $insert_sth, $update_sth) {
141        $_->{RaiseError} = 0;  # don't die on error
142        $_->{Active} = 1;
143        $_->finish;
144    }
145   
146    return $count;
147}
148
149sub sync_db {
150    my $self = shift;
151    my $db_file = shift or croak "Need the name of the databse to sync";
152   
153    my $dbh = DBI->connect("dbi:SQLite:dbname=$db_file", '', '', {RaiseError => 1});
154    my $select_sth = $dbh->prepare('SELECT FILENAME FROM mp3');
155    my $delete_sth = $dbh->prepare('DELETE FROM mp3 WHERE FILENAME = ?');
156   
157    # the number of records removed
158    my $count = 0;
159   
160    $select_sth->execute;
161    while (my ($filename) = $select_sth->fetchrow_array) {
162        unless (-e $filename) {
163            $delete_sth->execute($filename);
164            print STDERR "D $filename\n";
165            $count++;
166        }
167    }
168   
169    return $count;   
170}
171
172sub destroy_db {
173    my $self = shift;
174    my $db_file = shift or croak "Need the name of a database to destory";
175    unlink $db_file;
176}
177
178# module return
1791;
180
181=head1 NAME
182
183MP3::Find::DB - SQLite database backend to MP3::Find
184
185=head1 SYNOPSIS
186
187    use MP3::Find::DB;
188    my $finder = MP3::Find::DB->new;
189   
190    my @mp3s = $finder->find_mp3s(
191        dir => '/home/peter/music',
192        query => {
193            artist => 'ilyaimy',
194            album  => 'myxomatosis',
195        },
196        ignore_case => 1,
197        db_file => 'mp3.db',
198    );
199   
200    # you can do things besides just searching the database
201   
202    # create another database
203    $finder->create_db('my_mp3s.db');
204   
205    # update the database from the filesystem
206    $finder->update_db('my_mp3s.db', ['/home/peter/mp3', '/home/peter/cds']);
207   
208    # and then blow it away
209    $finder->destroy_db('my_mp3s.db');
210
211=head1 REQUIRES
212
213L<DBI>, L<DBD::SQLite>, L<SQL::Abstract>
214
215=head1 DESCRIPTION
216
217This is the SQLite database backend for L<MP3::Find>.
218
219B<Note:> I'm still working out some kinks in here, so this backend
220is currently not as stable as the Filesystem backend.
221
222=head2 Special Options
223
224=over
225
226=item C<db_file>
227
228The name of the SQLite database file to use. Defaults to F<~/mp3.db>.
229
230The database should have at least one table named C<mp3> with the
231following schema:
232
233    CREATE TABLE mp3 (
234        mtime         INTEGER,
235        FILENAME      TEXT,
236        TITLE         TEXT,
237        ARTIST        TEXT,
238        ALBUM         TEXT,
239        YEAR          INTEGER,
240        COMMENT       TEXT,
241        GENRE         TEXT,
242        TRACKNUM      INTEGER,
243        VERSION       NUMERIC,
244        LAYER         INTEGER,
245        STEREO        TEXT,
246        VBR           TEXT,
247        BITRATE       INTEGER,
248        FREQUENCY     INTEGER,
249        SIZE          INTEGER,
250        OFFSET        INTEGER,
251        SECS          INTEGER,
252        MM            INTEGER,
253        SS            INTEGER,
254        MS            INTEGER,
255        TIME          TEXT,
256        COPYRIGHT     TEXT,
257        PADDING       INTEGER,
258        MODE          INTEGER,
259        FRAMES        INTEGER,
260        FRAME_LENGTH  INTEGER,
261        VBR_SCALE     INTEGER
262    );
263
264=back
265
266=head1 METHODS
267
268=head2 create_db
269
270    $finder->create_db($db_filename);
271
272Creates a SQLite database in the file named c<$db_filename>.
273
274=head2 update_db
275
276    my $count = $finder->update_db($db_filename, \@dirs);
277
278Searches for all mp3 files in the directories named by C<@dirs>
279using L<MP3::Find::Filesystem>, and adds or updates the ID3 info
280from those files to the database. If a file already has a record
281in the database, then it will only be updated if it has been modified
282sinc ethe last time C<update_db> was run.
283
284=head2 sync_db
285
286    my $count = $finder->sync_db($db_filename);
287
288Removes entries from the database that refer to files that no longer
289exist in the filesystem. Returns the count of how many records were
290removed.
291
292=head2 destroy_db
293
294    $finder->destroy_db($db_filename);
295
296Permanantly removes the database.
297
298=head1 TODO
299
300Database maintanence routines (e.g. clear out old entries)
301
302Allow the passing of a DSN or an already created C<$dbh> instead
303of a SQLite database filename; or write driver classes to handle
304database dependent tasks (create_db/destroy_db).
305
306=head1 SEE ALSO
307
308L<MP3::Find>, L<MP3::Find::Filesystem>, L<mp3db>
309
310=head1 AUTHOR
311
312Peter Eichman <peichman@cpan.org>
313
314=head1 COPYRIGHT AND LICENSE
315
316Copyright (c) 2006 by Peter Eichman. All rights reserved.
317
318This program is free software; you can redistribute it and/or
319modify it under the same terms as Perl itself.
320
321=cut
Note: See TracBrowser for help on using the repository browser.