7 use Cwd
qw(cwd abs_path);
12 use Ogg
::Vorbis
::Header
;
15 'database' => 'adorno',
16 'dbuser' => 'adorno_app',
27 my $processed_count = 0; # Count the files we actually process.
32 $0 [options ...] [path ...]
35 -d | --database <dbname> Log to this database (default: "adorno")
36 -u | --user <username> Log to database as this user (default: $ENV{'USER'})
37 -s | --sleep <secs> Sleep for a second after adding this many files (default: 20)
38 -f | --force Index all files we find. (default: only index new files)
39 -q | --quiet Be quiet - don't output progress messages.
40 -r | --remove Remove all equal hash files that are not the file we are processing
41 -c | --clean Remove all equal hash files that are not the file we are processing
42 --debug Debugging information
43 --help Request this help text
47 # --update Update the hash (and all other information) on any pathname match (TODO)
48 # --duration Only update duration for existing tracks (still add new ones though) (TODO)
54 while( my $opt = shift ) {
56 if ( $opt =~ /^-(d|-database)$/ ) { $opts{'database'} = shift; }
57 elsif ( $opt =~ /^-(u|-user)$/ ) { $opts{'dbuser'} = shift; }
58 elsif ( $opt =~ /^-(s|-sleep)$/ ) { $opts{'sleep'} = shift; }
59 elsif ( $opt =~ /^-(f|-force)$/ ) { $opts{'force'} = 1; }
60 elsif ( $opt =~ /^-(q|-quiet)$/ ) { $opts{'quiet'} = 1; }
61 elsif ( $opt =~ /^-(r|-remove)$/ ) { $opts{'remove'} = 1; }
62 elsif ( $opt =~ /^-(c|-clean)$/ ) { $opts{'clean'} = 1; }
63 elsif ( $opt =~ /^--update$/ ) { $opts{'update'} = 1; }
64 elsif ( $opt =~ /^--duration$/ ) { $opts{'duration'} = 1; }
65 elsif ( $opt =~ /^--debug$/ ) { $opts{'debug'} = 1; }
66 elsif ( $opt =~ /^--help$/ ) { usage
(0); }
72 push @file_list, $opt ;
77 my $dbh = DBI
->connect("dbi:Pg:dbname=$opts{'database'}", $opts{'dbuser'} ) or die "Can't connect to database $opts{'database'}";
79 #################################################################
81 #################################################################
82 my $insert_track = $dbh->prepare( <<EOQ ) or die $dbh->errstr;
84 ( hash_key, path_name, title, artist, album, tracknum, setpart, duration, quality )
85 VALUES( ?, ?, ?, ?, ?, ?, ?, ?::interval, ? )
88 my $clean_tracks_by_path = $dbh->prepare( <<EOQ ) or die $dbh->errstr;
89 DELETE FROM tracks WHERE path_name LIKE ?
92 my $delete_track_by_path = $dbh->prepare( <<EOQ ) or die $dbh->errstr;
93 DELETE FROM tracks WHERE path_name = ?
96 my $select_hash_by_path = $dbh->prepare( <<EOQ ) or die $dbh->errstr;
97 SELECT hash_key FROM tracks WHERE path_name = ?
100 my $delete_track_by_hash = $dbh->prepare( <<EOQ ) or die $dbh->errstr;
101 DELETE FROM tracks WHERE hash_key = ?
108 #################################################################
109 # Deal to a single file.
110 #################################################################
112 my ( $filename ) = @_;
114 return if ( $filename !~ /\.(ogg|mp3)$/ );
116 print "." unless( $opts{'quiet'} );
117 my $safe_path = $filename;
119 if ( $opts{'force'} ) {
120 $delete_track_by_path->execute( $safe_path ) or die $dbh->errstr;
123 $select_hash_by_path->execute( $safe_path ) or die $dbh->errstr;
124 if ( $select_hash_by_path->rows > 0 ) {
125 $select_hash_by_path->finish();
129 my @fstats = stat($filename);
131 open(FILE
, $filename) or die "Can't open '$filename': $!";
133 my $hashkey = Digest
::MD5
->new->addfile(*FILE
)->hexdigest;
135 print "#" unless( $opts{'quiet'} );
138 sleep(1) if ( $opts{'sleep'} > 0 && $processed_count % $opts{'sleep'} == 0 );
140 if ( $opts{'remove'} ) {
141 $delete_track_by_hash->execute( $hashkey ) or die $dbh->errstr;
144 my $tartist = "Unknown";
145 my $talbum = "Unknown";
146 my $ttitle = "Unknown";
151 my $tquality = "HUH: Appears dodgy!";
153 if ( $filename =~ /\.(ogg|mp3)$/ ) {
155 if ( $filename =~ /\.mp3$/ ) {
156 # Get the info out of the (hopefully) .mp3 file
157 my $mp3info = get_mp3info
($filename) or do {
158 print "\nNo MP3 info for $filename (the file is likely to be unplayable).";
161 my $mp3tag = get_mp3tag
($filename) or do {
162 print "\nNo ID3 tag info for $filename";
165 $tartist = $mp3tag->{'ARTIST'};
166 $talbum = $mp3tag->{'ALBUM'};
167 $ttitle = $mp3tag->{'TITLE'};
168 $tyear = $mp3tag->{'YEAR'};
169 $tnum = $mp3tag->{'TRACKNUM'};
170 $tlength = sprintf( "%02dm:%02d.%03ds", $mp3info->{'MM'}, $mp3info->{'SS'}, $mp3info->{'MS'} );
171 $tquality = sprintf( "MP3: %s kbps, %s kHz, %s", $mp3info->{'BITRATE'}, $mp3info->{'FREQUENCY'}, ($mp3info->{'STEREO'} ?
"stereo" : "mono") );
175 # Get the info out of the (presumed) .ogg file
176 my $ogghdr = Ogg
::Vorbis
::Header
->load($filename) or do {
177 print "\nNo Ogg Vorbis info for $filename";
180 $tlength = sprintf( "%.1lf", $ogghdr->info('length'));
181 my $average_bitrate = ($fstats[7] / 125 ) / $tlength ;
182 $tquality = sprintf( "OGG: %0.0lf kbps, %0.1lf kHz, %s", $average_bitrate, $ogghdr->info('rate')/1000, ($ogghdr->info('channels') > 1 ?
"stereo" : "mono"));
183 $tlength = sprintf( "%.1lf", $ogghdr->info('length'));
185 foreach my $key ($ogghdr->comment_tags) {
186 foreach my $value ( $ogghdr->comment($key) ) {
187 printf( "[%s] = [%s]\n", $key, $value ) if ( $opts{'debug'} );
188 if ( $key =~ /^artist$/i ) { $tartist = $value; }
189 elsif ( $key =~ /^album$/i ) { $talbum = $value; }
190 elsif ( $key =~ /^title$/i ) { $ttitle = $value; }
191 elsif ( $key =~ /^tracknum(ber)?$/i ) { $tnum = $value; }
192 elsif ( $key =~ /^date$/i ) { $tyear = $value; }
197 # Now adjust some of those values a little further...
198 $tnum =~ s/\/.*$//; # in case it is 7/12 format
(i
.e
. track
7 of
12).
199 $tnum =~ s/[^0-9]//g ;
200 $tnum = 0 if ( $tnum eq "" );
202 $setpart = int($setpart);
204 my @query_args = ( $tartist, $talbum, $tnum, $ttitle, $tyear );
205 printf( "[%s]\n", join( '], [', @query_args )) if ( $opts{'debug'} );
207 $insert_track->execute( $hashkey, $safe_path, $ttitle, $tartist, $talbum, $tnum, $setpart, $tlength, $tquality ) or
214 #################################################################
215 # Recurse through a single directory
216 #################################################################
218 my ( $dirname ) = @_;
221 return if ( $dirname =~ /^abcde\./ ); # Skip random stuff abcde leaves around.
223 print "\nLooking in: $dirname" unless( $opts{'quiet'} );
224 if ( $opts{'clean'} ) {
225 $clean_tracks_by_path->execute( $dirname.'%' ) or die $dbh->errstr;
228 opendir( DIR
, $dirname );
229 @files = grep { ! /^\./ } readdir(DIR
);
232 foreach ( sort @files ) {
233 my $full_name = "$dirname/$_";
234 next if ( -l
$full_name ); # Skip symbolic links
235 if ( -f
$full_name ) {
236 one_file
( $full_name );
238 elsif ( -d
$full_name ) {
239 one_directory
( $full_name );
246 #################################################################
249 #################################################################
250 if ( $#file_list < 0 ) {
251 push @file_list, cwd
();
254 foreach ( @file_list ) {
257 my $abs_path = abs_path
($_);
259 if ( -f
$abs_path ) {
260 one_file
( $abs_path );
262 elsif( -d
$abs_path ) {
263 one_directory
( $abs_path );
268 print "\nDealt to $processed_count files.\n";