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

Last change on this file since 19 was 19, checked in by peter, 18 years ago
  • added "status_callback" option to the constructor for MP3::Find::DB
  • default status callback just prints the status code and the file name to STDERR as before
  • using an empty status_callback in t/03-db.t (status_callback => sub {}) to keep the test output more legible
  • updated docs in Find.pm
  • mkreadme now prints a blank line between the file header and the DESCRIPION section
  • generated new README file
File size: 9.1 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 create_db
278
279    $finder->create_db($db_filename);
280
281Creates a SQLite database in the file named c<$db_filename>.
282
283=head2 update_db
284
285    my $count = $finder->update_db($db_filename, \@dirs);
286
287Searches for all mp3 files in the directories named by C<@dirs>
288using L<MP3::Find::Filesystem>, and adds or updates the ID3 info
289from those files to the database. If a file already has a record
290in the database, then it will only be updated if it has been modified
291sinc ethe last time C<update_db> was run.
292
293=head2 sync_db
294
295    my $count = $finder->sync_db($db_filename);
296
297Removes entries from the database that refer to files that no longer
298exist in the filesystem. Returns the count of how many records were
299removed.
300
301=head2 destroy_db
302
303    $finder->destroy_db($db_filename);
304
305Permanantly removes the database.
306
307=head1 TODO
308
309Database maintanence routines (e.g. clear out old entries)
310
311Allow the passing of a DSN or an already created C<$dbh> instead
312of a SQLite database filename; or write driver classes to handle
313database dependent tasks (create_db/destroy_db).
314
315=head1 SEE ALSO
316
317L<MP3::Find>, L<MP3::Find::Filesystem>, L<mp3db>
318
319=head1 AUTHOR
320
321Peter Eichman <peichman@cpan.org>
322
323=head1 COPYRIGHT AND LICENSE
324
325Copyright (c) 2006 by Peter Eichman. All rights reserved.
326
327This program is free software; you can redistribute it and/or
328modify it under the same terms as Perl itself.
329
330=cut
Note: See TracBrowser for help on using the repository browser.