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

Last change on this file since 20 was 20, checked in by peter, 18 years ago
  • documented "status_callback" in DB.pm
  • updated Changes
File size: 9.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
45my $DEFAULT_STATUS_CALLBACK = sub {
46    my ($action_code, $filename) = @_;
47    print STDERR "$action_code $filename\n";
48};
49
50sub search {
51    my $self = shift;
52    my ($query, $dirs, $sort, $options) = @_;
53   
54    croak 'Need a database name to search (set "db_file" in the call to find_mp3s)' unless $$options{db_file};
55   
56    my $dbh = DBI->connect("dbi:SQLite:dbname=$$options{db_file}", '', '', {RaiseError => 1});
57   
58    # use the 'LIKE' operator to ignore case
59    my $op = $$options{ignore_case} ? 'LIKE' : '=';
60   
61    # add the SQL '%' wildcard to match substrings
62    unless ($$options{exact_match}) {
63        for my $value (values %$query) {
64            $value = [ map { "%$_%" } @$value ];
65        }
66    }
67
68    my ($where, @bind) = $sql->where(
69        { map { $_ => { $op => $query->{$_} } } keys %$query },
70        ( @$sort ? [ map { uc } @$sort ] : () ),
71    );
72   
73    my $select = "SELECT * FROM mp3 $where";
74   
75    my $sth = $dbh->prepare($select);
76    $sth->execute(@bind);
77   
78    my @results;
79    while (my $row = $sth->fetchrow_hashref) {
80        push @results, $row;
81    }
82   
83    return @results;
84}
85
86sub create_db {
87    my $self = shift;
88    my $db_file = shift or croak "Need a name for the database I'm about to create";
89    my $dbh = DBI->connect("dbi:SQLite:dbname=$db_file", '', '', {RaiseError => 1});
90    $dbh->do('CREATE TABLE mp3 (' . join(',', map { "$$_[0] $$_[1]" } @COLUMNS) . ')');
91}
92
93sub update_db {
94    my $self = shift;
95    my $db_file = shift or croak "Need the name of the database to update";
96    my $dirs = shift;
97   
98    my $status_callback = $self->{status_callback} || $DEFAULT_STATUS_CALLBACK;
99   
100    my @dirs = ref $dirs eq 'ARRAY' ? @$dirs : ($dirs);
101   
102    my $dbh = DBI->connect("dbi:SQLite:dbname=$db_file", '', '', {RaiseError => 1});
103    my $mtime_sth = $dbh->prepare('SELECT mtime FROM mp3 WHERE FILENAME = ?');
104    my $insert_sth = $dbh->prepare(
105        'INSERT INTO mp3 (' . 
106            join(',', map { $$_[0] } @COLUMNS) .
107        ') VALUES (' .
108            join(',', map { '?' } @COLUMNS) .
109        ')'
110    );
111    my $update_sth = $dbh->prepare(
112        'UPDATE mp3 SET ' . 
113            join(',', map { "$$_[0] = ?" } @COLUMNS) . 
114        ' WHERE FILENAME = ?'
115    );
116   
117    # the number of records added or updated
118    my $count = 0;
119   
120    # look for mp3s using the filesystem backend
121    require MP3::Find::Filesystem;
122    my $finder = MP3::Find::Filesystem->new;
123    for my $mp3 ($finder->find_mp3s(dir => \@dirs, no_format => 1)) {
124        # see if the file has been modified since it was first put into the db
125        $mp3->{mtime} = (stat($mp3->{FILENAME}))[9];
126        $mtime_sth->execute($mp3->{FILENAME});
127        my $records = $mtime_sth->fetchall_arrayref;
128       
129        warn "Multiple records for $$mp3{FILENAME}\n" if @$records > 1;
130       
131        #TODO: maybe print status updates somewhere else?
132        if (@$records == 0) {
133            $insert_sth->execute(map { $mp3->{$$_[0]} } @COLUMNS);
134            $status_callback->(A => $$mp3{FILENAME});
135            $count++;
136        } elsif ($mp3->{mtime} > $$records[0][0]) {
137            # the mp3 file is newer than its record
138            $update_sth->execute((map { $mp3->{$$_[0]} } @COLUMNS), $mp3->{FILENAME});
139            $status_callback->(U => $$mp3{FILENAME});
140            $count++;
141        }
142    }
143   
144    # as a workaround for the 'closing dbh with active staement handles warning
145    # (see http://rt.cpan.org/Ticket/Display.html?id=9643#txn-120724)
146    foreach ($mtime_sth, $insert_sth, $update_sth) {
147        $_->{RaiseError} = 0;  # don't die on error
148        $_->{PrintError} = 0;  # ...and don't even say anything
149        $_->{Active} = 1;
150        $_->finish;
151    }
152   
153    return $count;
154}
155
156sub sync_db {
157    my $self = shift;
158    my $db_file = shift or croak "Need the name of the databse to sync";
159
160    my $status_callback = $self->{status_callback} || $DEFAULT_STATUS_CALLBACK;
161
162    my $dbh = DBI->connect("dbi:SQLite:dbname=$db_file", '', '', {RaiseError => 1});
163    my $select_sth = $dbh->prepare('SELECT FILENAME FROM mp3');
164    my $delete_sth = $dbh->prepare('DELETE FROM mp3 WHERE FILENAME = ?');
165   
166    # the number of records removed
167    my $count = 0;
168   
169    $select_sth->execute;
170    while (my ($filename) = $select_sth->fetchrow_array) {
171        unless (-e $filename) {
172            $delete_sth->execute($filename);
173            $status_callback->(D => $filename);
174            $count++;
175        }
176    }
177   
178    return $count;   
179}
180
181sub destroy_db {
182    my $self = shift;
183    my $db_file = shift or croak "Need the name of a database to destory";
184    unlink $db_file;
185}
186
187# module return
1881;
189
190=head1 NAME
191
192MP3::Find::DB - SQLite database backend to MP3::Find
193
194=head1 SYNOPSIS
195
196    use MP3::Find::DB;
197    my $finder = MP3::Find::DB->new;
198   
199    my @mp3s = $finder->find_mp3s(
200        dir => '/home/peter/music',
201        query => {
202            artist => 'ilyaimy',
203            album  => 'myxomatosis',
204        },
205        ignore_case => 1,
206        db_file => 'mp3.db',
207    );
208   
209    # you can do things besides just searching the database
210   
211    # create another database
212    $finder->create_db('my_mp3s.db');
213   
214    # update the database from the filesystem
215    $finder->update_db('my_mp3s.db', ['/home/peter/mp3', '/home/peter/cds']);
216   
217    # and then blow it away
218    $finder->destroy_db('my_mp3s.db');
219
220=head1 REQUIRES
221
222L<DBI>, L<DBD::SQLite>, L<SQL::Abstract>
223
224=head1 DESCRIPTION
225
226This is the SQLite database backend for L<MP3::Find>.
227
228B<Note:> I'm still working out some kinks in here, so this backend
229is currently not as stable as the Filesystem backend.
230
231=head2 Special Options
232
233=over
234
235=item C<db_file>
236
237The name of the SQLite database file to use. Defaults to F<~/mp3.db>.
238
239The database should have at least one table named C<mp3> with the
240following schema:
241
242    CREATE TABLE mp3 (
243        mtime         INTEGER,
244        FILENAME      TEXT,
245        TITLE         TEXT,
246        ARTIST        TEXT,
247        ALBUM         TEXT,
248        YEAR          INTEGER,
249        COMMENT       TEXT,
250        GENRE         TEXT,
251        TRACKNUM      INTEGER,
252        VERSION       NUMERIC,
253        LAYER         INTEGER,
254        STEREO        TEXT,
255        VBR           TEXT,
256        BITRATE       INTEGER,
257        FREQUENCY     INTEGER,
258        SIZE          INTEGER,
259        OFFSET        INTEGER,
260        SECS          INTEGER,
261        MM            INTEGER,
262        SS            INTEGER,
263        MS            INTEGER,
264        TIME          TEXT,
265        COPYRIGHT     TEXT,
266        PADDING       INTEGER,
267        MODE          INTEGER,
268        FRAMES        INTEGER,
269        FRAME_LENGTH  INTEGER,
270        VBR_SCALE     INTEGER
271    );
272
273=back
274
275=head1 METHODS
276
277=head2 new
278
279    my $finder = MP3::Find::DB->new(
280        status_callback => \&callback,
281    );
282
283The C<status_callback> gets called each time an entry in the
284database is added, updated, or deleted by the C<update_db> and
285C<sync_db> methods. The arguments passed to the callback are
286a status code (A, U, or D) and the filename for that entry.
287The default callback just prints these to C<STDERR>:
288
289    sub default_callback {
290        my ($status_code, $filename) = @_;
291        print STDERR "$status_code $filename\n";
292    }
293
294To suppress any output, set C<status_callback> to an empty sub:
295
296    status_callback => sub {}
297
298=head2 create_db
299
300    $finder->create_db($db_filename);
301
302Creates a SQLite database in the file named c<$db_filename>.
303
304=head2 update_db
305
306    my $count = $finder->update_db($db_filename, \@dirs);
307
308Searches for all mp3 files in the directories named by C<@dirs>
309using L<MP3::Find::Filesystem>, and adds or updates the ID3 info
310from those files to the database. If a file already has a record
311in the database, then it will only be updated if it has been modified
312sinc ethe last time C<update_db> was run.
313
314=head2 sync_db
315
316    my $count = $finder->sync_db($db_filename);
317
318Removes entries from the database that refer to files that no longer
319exist in the filesystem. Returns the count of how many records were
320removed.
321
322=head2 destroy_db
323
324    $finder->destroy_db($db_filename);
325
326Permanantly removes the database.
327
328=head1 TODO
329
330Database maintanence routines (e.g. clear out old entries)
331
332Allow the passing of a DSN or an already created C<$dbh> instead
333of a SQLite database filename; or write driver classes to handle
334database dependent tasks (create_db/destroy_db).
335
336=head1 SEE ALSO
337
338L<MP3::Find>, L<MP3::Find::Filesystem>, L<mp3db>
339
340=head1 AUTHOR
341
342Peter Eichman <peichman@cpan.org>
343
344=head1 COPYRIGHT AND LICENSE
345
346Copyright (c) 2006 by Peter Eichman. All rights reserved.
347
348This program is free software; you can redistribute it and/or
349modify it under the same terms as Perl itself.
350
351=cut
Note: See TracBrowser for help on using the repository browser.