Quanta droppings.
[adorno.git] / scripts / add_subtree.pl
blob665731e698f89286de075bb7f80a15f2e25f8374
1 #!/usr/bin/perl
3 use strict;
4 use warnings;
5 use Carp;
7 use Cwd qw(cwd abs_path);
8 use Digest::MD5;
9 use DBI;
11 use MP3::Info;
12 use Ogg::Vorbis::Header;
14 my %opts = (
15 database => 'adorno',
16 dbuser => 'general',
17 force => 0,
18 remove => 0,
19 clean => 0,
20 update => 0,
21 quiet => 0,
22 sleep => 20,
23 duration => 0,
24 debug => 0 );
26 my @file_list;
27 my $processed_count = 0; # Count the files we actually process.
29 sub usage {
30 print <<EOUSAGE ;
31 Usage:
32 $0 [options ...] [path ...]
34 Options:
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
45 EOUSAGE
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)
50 exit shift;
54 while( my $opt = shift ) {
55 if ( $opt =~ /^-/ ) {
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); }
67 else {
68 usage(1);
71 else {
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 #################################################################
80 # Prepare queries
81 #################################################################
82 my $insert_track = $dbh->prepare( <<EOQ ) or die $dbh->errstr;
83 INSERT INTO tracks
84 ( hash_key, path_name, title, artist, album, tracknum, setpart, duration, quality )
85 VALUES( ?, ?, ?, ?, ?, ?, ?, ?::interval, ? )
86 EOQ
88 my $clean_tracks_by_path = $dbh->prepare( <<EOQ ) or die $dbh->errstr;
89 DELETE FROM tracks WHERE path_name LIKE ?
90 EOQ
92 my $delete_track_by_path = $dbh->prepare( <<EOQ ) or die $dbh->errstr;
93 DELETE FROM tracks WHERE path_name = ?
94 EOQ
96 my $select_hash_by_path = $dbh->prepare( <<EOQ ) or die $dbh->errstr;
97 SELECT hash_key FROM tracks WHERE path_name = ?
98 EOQ
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 #################################################################
111 sub one_file {
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;
122 else {
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();
126 return;
129 my @fstats = stat($filename);
131 open(FILE, $filename) or die "Can't open '$filename': $!";
132 binmode(FILE);
133 my $hashkey = Digest::MD5->new->addfile(*FILE)->hexdigest;
134 close(FILE);
135 print "#" unless( $opts{'quiet'} );
137 $processed_count++;
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";
147 my $tyear = "1066";
148 my $tnum = 0;
149 my $setpart = 1;
150 my $tlength = 0;
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).";
159 return;
161 my $mp3tag = get_mp3tag($filename) or do {
162 print "\nNo ID3 tag info for $filename";
163 return;
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") );
174 else {
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";
178 return;
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 if ( $key =~ /artist/i ) { $tartist = $value; }
188 elsif ( $key =~ /album/i ) { $talbum = $value; }
189 elsif ( $key =~ /title/i ) { $ttitle = $value; }
190 elsif ( $key =~ /tracknum/i ) { $tnum = $value; }
191 elsif ( $key =~ /date/i ) { $tyear = $value; }
196 # Now adjust some of those values a little further...
197 $tnum =~ s/\/.*$//; # in case it is 7/12 format (i.e. track 7 of 12).
198 $tnum =~ s/[^0-9]//g ;
199 $tnum = 0 if ( $tnum eq "" );
200 $tnum = int($tnum);
201 $setpart = int($setpart);
203 $insert_track->execute( $hashkey, $safe_path, $ttitle, $tartist, $talbum, $tnum, $setpart, $tlength, $tquality) or
204 die $dbh->errstr;
210 #################################################################
211 # Recurse through a single directory
212 #################################################################
213 sub one_directory {
214 my ( $dirname ) = @_;
215 my @files;
217 return if ( $dirname =~ /^abcde\./ ); # Skip random stuff abcde leaves around.
219 print "\nLooking in: $dirname" unless( $opts{'quiet'} );
220 if ( $opts{'clean'} ) {
221 $clean_tracks_by_path->execute( $dirname.'%' ) or die $dbh->errstr;
224 opendir( DIR, $dirname );
225 @files = grep { ! /^\./ } readdir(DIR);
226 closedir DIR;
228 foreach ( sort @files ) {
229 my $full_name = "$dirname/$_";
230 next if ( -l $full_name ); # Skip symbolic links
231 if ( -f $full_name ) {
232 one_file( $full_name );
234 elsif ( -d $full_name ) {
235 one_directory( $full_name );
242 #################################################################
243 # Main code...
245 #################################################################
246 if ( $#file_list < 0 ) {
247 push @file_list, cwd();
250 foreach ( @file_list ) {
251 s/\/$// ;
253 my $abs_path = abs_path($_);
255 if ( -f $abs_path ) {
256 one_file( $abs_path );
258 elsif( -d $abs_path ) {
259 one_directory( $abs_path );
264 print "\nDealt to $processed_count files.\n";