| 1 | package MP3::Find::Filesystem; | 
|---|
| 2 |  | 
|---|
| 3 | use strict; | 
|---|
| 4 | use warnings; | 
|---|
| 5 |  | 
|---|
| 6 | use base 'MP3::Find::Base'; | 
|---|
| 7 |  | 
|---|
| 8 | use File::Find; | 
|---|
| 9 | use MP3::Info; | 
|---|
| 10 | use Scalar::Util qw(looks_like_number); | 
|---|
| 11 |  | 
|---|
| 12 | eval { | 
|---|
| 13 |     require Sort::Key; | 
|---|
| 14 |     Sort::Key->import(qw(multikeysorter)); | 
|---|
| 15 |     use Sort::Key::Natural; | 
|---|
| 16 | }; | 
|---|
| 17 | my $USE_SORT_KEY = $@ ? 0 : 1; | 
|---|
| 18 |  | 
|---|
| 19 |  | 
|---|
| 20 | eval { require MP3::Tag }; | 
|---|
| 21 | my $CAN_USE_ID3V2 = $@ ? 0 : 1; | 
|---|
| 22 |  | 
|---|
| 23 | use_winamp_genres(); | 
|---|
| 24 |  | 
|---|
| 25 | sub search { | 
|---|
| 26 |     my $self = shift; | 
|---|
| 27 |     my ($query, $dirs, $sort, $options) = @_; | 
|---|
| 28 |      | 
|---|
| 29 |     # prep the search patterns as regexes | 
|---|
| 30 |     foreach (keys(%$query)) { | 
|---|
| 31 |         my $ref = ref $$query{$_}; | 
|---|
| 32 |         # make arrays into 'OR' searches | 
|---|
| 33 |         if ($ref eq 'ARRAY') { | 
|---|
| 34 |             $$query{$_} = '(' . join('|', @{ $$query{$_} }) . ')'; | 
|---|
| 35 |         } | 
|---|
| 36 |         # convert to a regex unless it already IS a regex         | 
|---|
| 37 |         unless ($ref eq 'Regexp') { | 
|---|
| 38 |             $$query{$_} = "^$$query{$_}\$" if $$options{exact_match}; | 
|---|
| 39 |             $$query{$_} = $$options{ignore_case} ? qr[$$query{$_}]i : qr[$$query{$_}]; | 
|---|
| 40 |         } | 
|---|
| 41 |     } | 
|---|
| 42 |      | 
|---|
| 43 |     if ($$options{exclude_path}) { | 
|---|
| 44 |         my $ref = ref $$options{exclude_path}; | 
|---|
| 45 |         if ($ref eq 'ARRAY') { | 
|---|
| 46 |             $$options{exclude_path} = '(' . join('|', @{ $$options{exclude_path} }) . ')'; | 
|---|
| 47 |         } | 
|---|
| 48 |         unless ($ref eq 'Regexp') { | 
|---|
| 49 |             $$options{exclude_path} = qr[$$options{exclude_path}]; | 
|---|
| 50 |         } | 
|---|
| 51 |     } | 
|---|
| 52 |      | 
|---|
| 53 |     if ($$options{use_id3v2} and not $CAN_USE_ID3V2) { | 
|---|
| 54 |         # they want to use ID3v2, but don't have MP3::Tag | 
|---|
| 55 |         warn "MP3::Tag is required to search ID3v2 tags\n"; | 
|---|
| 56 |     } | 
|---|
| 57 |          | 
|---|
| 58 |     # run the actual find | 
|---|
| 59 |     my @results; | 
|---|
| 60 |     find(sub { match_mp3($File::Find::name, $query, \@results, $options) }, $_) foreach @$dirs; | 
|---|
| 61 |      | 
|---|
| 62 |     # sort the results | 
|---|
| 63 |     if (@$sort) { | 
|---|
| 64 |         if ($USE_SORT_KEY) { | 
|---|
| 65 |             # use Sort::Key to do a (hopefully!) faster sort | 
|---|
| 66 |             #TODO: profile this; at first glance, it doesn't actually seem to be any faster | 
|---|
| 67 |             #warn "Using Sort::Key"; | 
|---|
| 68 |             my $sorter = multikeysorter( | 
|---|
| 69 |                 sub { my $info = $_; map { $info->{uc $_} } @$sort }, | 
|---|
| 70 |                 map { 'natural' } @$sort | 
|---|
| 71 |             ); | 
|---|
| 72 |             @results = $sorter->(@results); | 
|---|
| 73 |         } else { | 
|---|
| 74 |             @results = sort { | 
|---|
| 75 |                 my $compare; | 
|---|
| 76 |                 foreach (map { uc } @$sort) { | 
|---|
| 77 |                     # use Scalar::Util to do the right sort of comparison | 
|---|
| 78 |                     $compare = (looks_like_number($a->{$_}) && looks_like_number($b->{$_})) ? | 
|---|
| 79 |                         $a->{$_} <=> $b->{$_} : | 
|---|
| 80 |                         $a->{$_} cmp $b->{$_}; | 
|---|
| 81 |                     # we found a field they differ on | 
|---|
| 82 |                     last if $compare; | 
|---|
| 83 |                 } | 
|---|
| 84 |                 return $compare; | 
|---|
| 85 |             } @results; | 
|---|
| 86 |         } | 
|---|
| 87 |     } | 
|---|
| 88 |      | 
|---|
| 89 |     return @results | 
|---|
| 90 | } | 
|---|
| 91 |  | 
|---|
| 92 | sub match_mp3 { | 
|---|
| 93 |     my ($filename, $query, $results, $options) = @_; | 
|---|
| 94 |      | 
|---|
| 95 |     return unless $filename =~ m{[^/]\.mp3$}; | 
|---|
| 96 |     if ($$options{exclude_path}) { | 
|---|
| 97 |         return if $filename =~ $$options{exclude_path}; | 
|---|
| 98 |     } | 
|---|
| 99 |      | 
|---|
| 100 |     my $mp3 = { | 
|---|
| 101 |         FILENAME => $filename, | 
|---|
| 102 |         %{ get_mp3tag($filename)  || {} }, | 
|---|
| 103 |         %{ get_mp3info($filename) || {} }, | 
|---|
| 104 |     }; | 
|---|
| 105 |      | 
|---|
| 106 |     if ($CAN_USE_ID3V2 and $$options{use_id3v2}) { | 
|---|
| 107 |         # add ID3v2 tag info, if present | 
|---|
| 108 |         my $mp3_tags = MP3::Tag->new($filename); | 
|---|
| 109 |         unless (defined $mp3_tags) { | 
|---|
| 110 |             warn "Can't get MP3::Tag object for $filename\n"; | 
|---|
| 111 |         } else { | 
|---|
| 112 |             $mp3_tags->get_tags; | 
|---|
| 113 |             if (my $id3v2 = $mp3_tags->{ID3v2}) { | 
|---|
| 114 |                 for my $frame_id (keys %{ $id3v2->get_frame_ids }) { | 
|---|
| 115 |                     my ($info) = $id3v2->get_frame($frame_id); | 
|---|
| 116 |                     if (ref $info eq 'HASH') { | 
|---|
| 117 |                         # use the "Text" value as the value for this frame, if present | 
|---|
| 118 |                         $mp3->{$frame_id} = $info->{Text} if exists $info->{Text}; | 
|---|
| 119 |                     } else { | 
|---|
| 120 |                         $mp3->{$frame_id} = $info; | 
|---|
| 121 |                     } | 
|---|
| 122 |                 } | 
|---|
| 123 |             } | 
|---|
| 124 |         } | 
|---|
| 125 |     } | 
|---|
| 126 |  | 
|---|
| 127 |     for my $field (keys(%{ $query })) { | 
|---|
| 128 |         my $value = $mp3->{uc($field)}; | 
|---|
| 129 |         return unless defined $value; | 
|---|
| 130 |         return unless $value =~ $query->{$field}; | 
|---|
| 131 |     } | 
|---|
| 132 |      | 
|---|
| 133 |     push @{ $results }, $mp3; | 
|---|
| 134 | } | 
|---|
| 135 |  | 
|---|
| 136 | # module return | 
|---|
| 137 | 1; | 
|---|
| 138 |  | 
|---|
| 139 | =head1 NAME | 
|---|
| 140 |  | 
|---|
| 141 | MP3::Find::Filesystem - File::Find-based backend to MP3::Find | 
|---|
| 142 |  | 
|---|
| 143 | =head1 SYNOPSIS | 
|---|
| 144 |  | 
|---|
| 145 |     use MP3::Find::Filesystem; | 
|---|
| 146 |     my $finder = MP3::Find::Filesystem->new; | 
|---|
| 147 |      | 
|---|
| 148 |     my @mp3s = $finder->find_mp3s( | 
|---|
| 149 |         dir => '/home/peter/music', | 
|---|
| 150 |         query => { | 
|---|
| 151 |             artist => 'ilyaimy', | 
|---|
| 152 |             album  => 'myxomatosis', | 
|---|
| 153 |         }, | 
|---|
| 154 |         ignore_case => 1, | 
|---|
| 155 |     ); | 
|---|
| 156 |  | 
|---|
| 157 | =head1 REQUIRES | 
|---|
| 158 |  | 
|---|
| 159 | L<File::Find>, L<MP3::Info>, L<Scalar::Util> | 
|---|
| 160 |  | 
|---|
| 161 | L<MP3::Tag> is also needed if you want to search using ID3v2 tags. | 
|---|
| 162 |  | 
|---|
| 163 | =head1 DESCRIPTION | 
|---|
| 164 |  | 
|---|
| 165 | This module implements the C<search> method from L<MP3::Find::Base> | 
|---|
| 166 | using a L<File::Find> based search of the local filesystem. | 
|---|
| 167 |  | 
|---|
| 168 | =head2 Special Options | 
|---|
| 169 |  | 
|---|
| 170 | =over | 
|---|
| 171 |  | 
|---|
| 172 | =item C<exclude_path> | 
|---|
| 173 |  | 
|---|
| 174 | Scalar or arrayref; any file whose name matches any of these paths | 
|---|
| 175 | will be skipped. | 
|---|
| 176 |  | 
|---|
| 177 | =item C<use_id3v2> | 
|---|
| 178 |  | 
|---|
| 179 | Boolean, defaults to false. If set to true, MP3::Find::Filesystem will | 
|---|
| 180 | use L<MP3::Tag> to get the ID3v2 tag for each file. You can then search | 
|---|
| 181 | for files by their ID3v2 data, using the four-character frame names.  | 
|---|
| 182 | This isn't very useful if you are just search by artist or title, but if, | 
|---|
| 183 | for example, you have made use of the C<TOPE> ("Orignal Performer") frame, | 
|---|
| 184 | you could search for all the cover songs in your collection: | 
|---|
| 185 |  | 
|---|
| 186 |     $finder->find_mp3s(query => { tope => '.' }); | 
|---|
| 187 |  | 
|---|
| 188 | As with the basic query keys, ID3v2 query keys are converted to uppercase | 
|---|
| 189 | internally. | 
|---|
| 190 |  | 
|---|
| 191 | =back | 
|---|
| 192 |  | 
|---|
| 193 | =head1 SEE ALSO | 
|---|
| 194 |  | 
|---|
| 195 | L<MP3::Find>, L<MP3::Find::DB> | 
|---|
| 196 |  | 
|---|
| 197 | =head1 AUTHOR | 
|---|
| 198 |  | 
|---|
| 199 | Peter Eichman <peichman@cpan.org> | 
|---|
| 200 |  | 
|---|
| 201 | =head1 COPYRIGHT AND LICENSE | 
|---|
| 202 |  | 
|---|
| 203 | Copyright (c) 2006 by Peter Eichman. All rights reserved. | 
|---|
| 204 |  | 
|---|
| 205 | This program is free software; you can redistribute it and/or | 
|---|
| 206 | modify it under the same terms as Perl itself. | 
|---|
| 207 |  | 
|---|
| 208 | =cut | 
|---|