#!/usr/bin/perl -w # Copyright © 2001-2007 Jamie Zawinski # # Permission to use, copy, modify, distribute, and sell this software and its # documentation for any purpose is hereby granted without fee, provided that # the above copyright notice appear in all copies and that both that # copyright notice and this permission notice appear in supporting # documentation. No representations are made about the suitability of this # software for any purpose. It is provided "as is" without express or # implied warranty. # # Created: 16-Jun-01. # # Given a set of MP3 file names, creates an HTML page that indexes into # them. # # depends on: "utils/menuify.pl", "cmp" require 5; use diagnostics; use strict; # Utter foulness! Without this, [:upper:] doesn't work on Latin1 characters. use locale; use POSIX qw(locale_h mktime); setlocale(LC_ALL, "en_US"); use POSIX qw(strftime); my $progname = $0; $progname =~ s@.*/@@g; my $version = q{ $Revision: 1.38 $ }; $version =~ s/^[^0-9]+([0-9.]+).*$/$1/; my $verbose = 0; my $audio_url_base = "http://cerebrum.dnalounge.com:8001/audio"; my $file_ext = "m3u"; my $file_bytes_per_second = 16000; # the magic number for 128k mp3 files... my $data_dir = $0; $data_dir =~ s@/[^/]*$@@; my $exec_dir = "$data_dir/utils"; my $html_base = "http://www.dnalounge.com/"; my @menuify_cmd = ("$exec_dir/menuify.pl", "$exec_dir/template.html", "--base", $html_base); my $intro_file = "$data_dir/intro.html"; my $max_age = 90; # ignore any files older than this many days sub error($) { my ($e) = @_; print STDERR "$progname: $e\n"; exit 1; } my %start_times = (); sub sort_files(@) { my (@files) = @_; my @files2 = (); foreach my $f (@files) { if (-l $f) { print STDERR "$progname: skipping symlink $f\n" if ($verbose > 1); next; } push @files2, $f; my $tf = $f; $tf =~ s@\.[^./]+$@.time@; # If we have a .time file, use its write date. # my $tt = (stat($tf))[9]; # Otherwise, use the write date of the mp3 file, *minus* the length # in seconds of that file (that is, assume the write date is the date # at which the last data was written.) # if (!$tt) { my @st = stat($f); error "$f does not exist\n" unless ($st[9]); $tt = $st[9] - (int($st[7] / $file_bytes_per_second)); } $start_times{$f} = $tt; } return sort { $start_times{$b} <=> $start_times{$a} } @files2; } # do the AM/PM thingy. sub to12($) { my ($n) = @_; $n %= 24; return ($n == 0 ? 12 : $n <= 12 ? $n : $n-12); } sub make_event_description($) { my ($audio_file) = @_; my $blurb = ""; my $blurb_file = $audio_file; if ($blurb_file =~ s@\.[^./]+$@.desc@) { local *IN; if (open (IN, "<$blurb_file")) { print STDERR "$progname: reading $blurb_file\n" if ($verbose > 1); while () { $blurb .= $_; } close IN; } } # if the first line has an html file name on it, use that as a link # to this event's flyer. # if ($blurb =~ s/^([^\s]+\.html)\n//si) { my $flyer = $1; $_ = $blurb_file; my ($yyyy, $mm, $dd) = m@/(\d{4})/(\d\d)-(\d\d)b?\.[a-z\d]+$@i; my $url = "${html_base}flyers/$yyyy/$mm/$flyer"; print STDERR "$progname: flyer $url\n" if ($verbose > 2); # wrap an HREF around the first ... $blurb =~ s@(()(.*?)())@$1@si; } $blurb =~ s/\s*(<(BR|P)>)\s*/$1/gs; # lose whitespace around breaks $blurb =~ s/^\s+//gs; $blurb =~ s/\s+$//gs; return $blurb; } sub make_duration_description($$$$) { my ($start_time, $duration, $real_duration, $detailp) = @_; my @tt = localtime($start_time); my $do_missing_p = ($detailp > 1); my $cal_href = sprintf ("${html_base}calendar/%04d/%02d.html\#%02d", $tt[5]+1900, $tt[4]+1, $tt[3]); my $output = ""; my $cal = ("" . strftime("%a, %e-%b-%Y", @tt) . ""); if ($detailp < 0) { return $cal; } $output .= ("\n" . " \n" . " \n" . " \n" . " \n" . " \n" . " \n" . " \n" . " \n" . " \n" . " \n" . " \n" . " \n" . " \n" . " \n" . " \n" . " \n" . " \n" . "
Date: $cal
Start: " . strftime("%l:%M %p", @tt) . "
End: " . strftime("%l:%M %p", localtime($start_time + $duration)) . "
Length: "); if ($real_duration > 0) { my $L = ($real_duration < 60 ? 60 : $real_duration); my $hh = int($L / (60 * 60)); my $mm = int($L / 60) % 60; # kludge for prettiness if ($mm == 59) { $mm = 0; $hh++; } if ($hh == 1) { $output .= "1 hour "; } elsif ($hh > 0) { $output .= "$hh hours "; } if ($mm == 1) { $output .= "1 minute "; } elsif ($mm > 0) { $output .= "$mm minutes "; } if ($do_missing_p) { my $missing = $duration - $real_duration; if ($missing > 60) { $output .= "
"; $output .= ""; $output .= "("; $hh = int($missing / (60 * 60)) % 60; $mm = int(($missing + 59) / 60) % 60; if ($hh == 1) { $output .= "1 hour "; } elsif ($hh > 0) { $output .= "$hh hours "; } if ($mm == 1) { $output .= "1 minute "; } elsif ($mm > 0) { $output .= "$mm minutes "; } $output .= " missing)"; $output .= ""; } } } else { $output .= "zero"; } $output .= ( "
\n"); return $output; } sub make_listen_link($$) { my ($url_base, $clickable) = @_; my $url = "$url_base-00000.$file_ext"; my $result = "Listen"; $result = ($clickable ? "$result" : "$result"); return $result; } sub make_sparse_listen_links($$$$$$) { my ($url_base, $interval, $start_time, $duration, $real_duration, $clickable) = @_; $duration -= $interval/2; $real_duration -= $interval/2; my $output = ""; $output .= make_listen_link ($url_base, $clickable); if ($real_duration <= $interval) { return $output; } $output .= "
\n"; $output .= "\n"; my @tt = localtime($start_time); my $start_h = $tt[2]; my $start_m = $tt[1]; my $start_s = $tt[0]; my $midnight = ($start_time - ($start_h * 60 * 60) - ($start_m * 60) - $start_s); my $off_from_hour = (($start_m > 30 ? (($start_m - 30) * 60) : ($start_m * 60)) + $start_s); my $last_timestr = ''; for (my $wall_clock = $start_time - $off_from_hour; $wall_clock < $start_time + $real_duration; $wall_clock += $interval) { my $wc2 = ($wall_clock < $start_time ? $start_time : $wall_clock); my $file_off = $wc2 - $start_time; # fudge "0:00:40" down to "0:00:00". $file_off = 0 if ($file_off < 59); my $offstr = sprintf("%d%02d%02d", int($file_off / (60 * 60)), int($file_off / 60) % 60, ($file_off % 60)); my $t2 = $wc2 - $midnight; my $timestr = sprintf("%d:%02d", to12(int($t2 / (60 * 60))), int($t2 / 60) % 60); next if ($timestr eq $last_timestr); # no dups... $last_timestr = $timestr; my $url = "$url_base-$offstr.$file_ext"; my $html = "$timestr \n"; $html = "  " . $html if ($timestr =~ m/^\d:\d\d$/s); # pad "9:30" $output .= $html; } $output .= "\n"; return $output; } sub make_listen_grid($$$$$) { my ($url_base, $interval, $start_time, $duration, $real_duration) = @_; # the grid shouldn't include fractional bits at the end: don't give me # a cell that contains less than $interval in it. # $duration -= $interval; $real_duration -= $interval; $duration = 0 if ($duration < 0); $real_duration = 0 if ($real_duration < 0); my @tt = localtime($start_time); my $start_h = $tt[2]; my $start_m = $tt[1]; my $start_s = $tt[0]; my $start_off = ($start_m * 60) + $start_s; # start on hour boundary my $grid_start = $start_time - $start_off; my $today_off = $grid_start - ($start_h * 60 * 60); my $end_time = $start_time + $duration; my $real_end_time = $start_time + $real_duration; my $cols = int ((60 * 60) / $interval); my $rows = int ((($end_time - $grid_start) + (60 * 60) - 1) / (60 * 60)); my $grid_end = $grid_start + ($rows * $cols * $interval); my $output = ""; if ($verbose > 3) { print STDERR "$progname: grid start: " . localtime($grid_start) . "\n"; print STDERR "$progname: data start: " . localtime($start_time) . "\n"; print STDERR "$progname: data end: " . localtime($real_end_time) . "\n"; print STDERR "$progname: desired end: " . localtime($end_time) . "\n"; print STDERR "$progname: grid end: " . localtime($grid_end) . "\n"; if ($end_time != $real_end_time) { my $m = $end_time - $real_end_time; print STDERR "$progname: missing: "; print STDERR sprintf("%d:%02d:%02d", int($m / (60 * 60)), int($m / 60) % 60, ($m % 60)); print STDERR "\n"; } print STDERR "\n"; } my $cell_time = $grid_start; my $started = 0; $output .= "\n"; for (my $y = 0; $y < $rows; $y++) { $output .= " \n"; for (my $x = 0; $x < $cols; $x++) { my $file_off = $cell_time - $start_time; my $today = $cell_time - $today_off; $output .= " \n"; $cell_time += $interval; } $output .= " \n"; } $output .= "
"; # fudge "0:00:40" down to "0:00:00". if (!$started && $file_off < 0 && $file_off > -59) { $file_off = 0; $started = 1; } if ($file_off >= 0 && $file_off < $duration) { # for the very first one, line it up with beginning of file. if (!$started && $file_off < $interval) { $today -= $file_off; $file_off = 0; $started = 1; } my $offstr = sprintf("%d%02d%02d", int($file_off / (60 * 60)), int($file_off / 60) % 60, ($file_off % 60)); my $timestr = sprintf("%d:%02d", to12(int($today / (60 * 60))), int($today / 60) % 60); my $url = "$url_base-$offstr.$file_ext"; my $open; my $close; if ($file_off < $real_duration) { $open = ""; $close = ""; } else { $open = ""; $close = ""; } $output .= ("" . "$open$timestr$close" . ""); } $output .= "
\n"; return $output; } sub make_index($@) { my ($detailp, @names) = @_; @names = sort_files(@names); my $output = ""; my $last_mon; my $last_year; my $count = 0; $output .= "\n" if ($detailp < 0); foreach my $f (@names) { my @st = stat($f); error "$f does not exist\n" unless ($st[9]); my $start = $start_times{$f}; my $end = $st[9]; my $zerop = $st[7] == 0; # $len is the difference between the first write, and the last write. # $real_len is the actual duration of the data in the file. my $real_len = int($st[7] / $file_bytes_per_second); my $len = $real_len; if ($end < $start + $len) { if ($verbose > 1) { print STDERR "$progname: warning: start/end times were out " . "of whack on $f:\n"; print STDERR "$progname: length: " . sprintf("%d:%02d:%02d\n", int($len / (60 * 60)), int($len / 60) % 60, ($len % 60)); print STDERR "$progname: start: " . localtime($start) . "\n"; print STDERR "$progname: end: " . localtime($end) . "\n"; print STDERR "$progname: start+len: " . localtime($start+$len) . "\n"; } $end = $start + $len; } else { $len = $end - $start; } next if ($f =~ m@\.time$@); if (! ($f =~ m@((\d\d\d\d)/(\d\d)-(\d\d)([a-z]?))\.mp3$@)) { print STDERR "$progname: WARNING: " . "skipping $f (unrecognised name format)\n"; next; } my $f2 = $1; my $year = $2; my $mon = $3; my $url_base = "$audio_url_base/$f2"; my $age_days = (time - $start) / (60 * 60 * 24); if ($detailp >= 0 && $age_days > $max_age) { if ($verbose > 1) { $age_days = int($age_days); print STDERR "$progname: skipping $f ($age_days days old)...\n"; } next; } print STDERR "$progname: indexing $f...\n" if ($verbose > 1); my $blurb = make_event_description ($f); if ($detailp < 0) { $blurb =~ s@().*$@$1@is || $blurb =~ s@().*$@$1@is || $blurb =~ s@().*$@@is; # capitalize words, from the perl faq... $blurb =~ s/((^\w)|(\s\w))/\U$1/g; $blurb =~ s/([\w\']+)/\u\L$1/g; # lowercase the rest $blurb =~ s/(<[^>]*)/\L$1/g; # oops, downcase inside all tags $blurb =~ s/(\&.)/\L$1/g; # oops, and entities $blurb =~ s@@@gi; my $size = $st[7]; if ($size > 2*1024*1024*1024) { $size = sprintf("%.1f GB", $size / (1024*1024*1024)); } elsif ($size > 1024*1024) { $size = sprintf("%d MB", $size / (1024*1024)); } elsif ($size > 1024) { $size = sprintf("%d KB", $size / 1024); } my $len_str = sprintf("%d:%02d", int($len / (60 * 60)), int($len / 60) % 60); if ($last_mon && $last_mon != $mon && $#names > 30) { $output .= "\n\n"; } $output .= ("\n\n"); } else { # ($detailp >= 0) if ($blurb ne "") { $output .= ("
" . "
" . "
" . "$size" . "" . "$len_str" . "" . make_duration_description ($start, $len, $real_len, $detailp) . "" . $blurb . "
\n" . " \n" . " \n" . " \n" . " \n" . "
\n" . $blurb . "\n"); } $output .= ("
\n" . " \n" . " \n" . " \n" . " \n" . " \n" . " \n"); } elsif ($detailp == 1) { my $right_p = 1; if ($right_p) { $output .= " \n"; $output .= " \n"; if ($right_p) { $output .= " \n"; } } else { # ($detailp > 1) $output .= "

\n"; $output .= make_listen_link ($url_base, !$zerop); $output .= "

\n"; $output .= " \n"; $output .= " \n"; } $output .= (" \n" . "
\n" . make_duration_description ($start, $len, $real_len, $detailp)); if ($detailp == 0) { $output .= ("
\n" . make_listen_link ($url_base, !$zerop) . "\n" . "
\n"; } else { $output .= "

\n"; } # my $interval = 60 * 60; # 1 hour my $interval = 30 * 60; # 30 minutes $output .= make_sparse_listen_links ($url_base, $interval, $start, $len, $real_len, !$zerop); $output .= "


\n"; my $interval = 60 * 5; # 5 minutes $output .= make_listen_grid ($url_base, $interval, $start, $len, $real_len); $output .= "
\n" . "
\n"); if ($blurb) { $output .= ("
\n"); } $output .= "

\n"; } $count++; $last_year = $year; $last_mon = $mon; } $output .= "\n" if ($detailp < 0); return $output; } sub indexer($$@) { my ($detailp, $outfile, @names) = @_; my $output = ""; $output .= "\n"; { local *IN; open(IN, "<$intro_file") || error "$intro_file: $!"; my $body = ""; while () { $body .= $_; } close IN; $body =~ s@@@gsi; # lose the NOWRAP comment $output .= $body; } if ($detailp <= 0) { $output =~ s@().*$@$1@s; $output .= "\nRaw MP3 Archive\n\n"; } $output .= "

\n"; $output .= make_index ($detailp, @names); if ($detailp < 0 && $outfile =~ m@/(\d\d\d\d)/@) { # kludge... $output =~ s@(HREF=\")$1/@$1@gi; } $output .= "\n"; local *OUT; my $file_tmp = "$outfile.tmp"; open(OUT, ">$file_tmp") || error "$file_tmp: $!"; print OUT $output || error "$file_tmp: $!"; close OUT; print STDERR "$progname: wrote $file_tmp\n" if ($verbose > 2); my @cmd = @menuify_cmd; if ($verbose > 2) { push @cmd, ("-" .("v" x ($verbose - 2))); } push @cmd, $file_tmp; print "$progname: executing \"" . join(" ", @cmd) . "\"\n" if ($verbose > 2); system @cmd; @cmd = ("cmp", "-s", "$file_tmp", "$outfile"); print STDERR "$progname: executing \"" . join(" ", @cmd) . "\"\n" if ($verbose > 2); if (system (@cmd)) { if (!rename ("$file_tmp", "$outfile")) { unlink "$file_tmp"; error "mv $file_tmp $outfile: $!"; } print STDERR "$progname: wrote $outfile\n" if ($verbose); } else { unlink "$file_tmp" || error "rm $file_tmp: $!\n"; print STDERR "$progname: $outfile unchanged\n" if ($verbose > 1); print STDERR "$progname: rm $file_tmp\n" if ($verbose > 2); } } sub check_files(@) { my (@names) = @_; @names = sort_files(@names); my $count = 0; foreach my $f (@names) { next unless ($f =~ m/\.mp3$/); my @st = stat($f); error "$f does not exist\n" unless ($st[9]); my $start = $start_times{$f}; my $end = $st[9]; $f =~ s@^.*/@@; # $len is the difference between the first write, and the last write. # $real_len is the actual duration of the data in the file. my $real_len = int($st[7] / $file_bytes_per_second); my $len = $real_len; # if ($end < $start + $len) { # print STDERR "$progname: warning: start/end times were out " . # "of whack on $f:\n"; # print STDERR "$progname: start: " . localtime($start) . "\n"; # print STDERR "$progname: end: " . localtime($end) . "\n"; # print STDERR "$progname: start+len: " . localtime($start+$len) . "\n"; # # $end = $start + $len; # } else { $len = $end - $start; # } my $ratio = ($real_len ? ($real_len / $len) : 0); my $missing_pct = (1 - $ratio) * 100; my $missing_secs = abs($len - $real_len); my $timestr = sprintf("%d:%02d:%02d", int($missing_secs / (60 * 60)), int($missing_secs / 60) % 60, ($missing_secs % 60)); if ($verbose || $ratio < 0.98 || $ratio > 1.0015) { if ($ratio == 1.0) { print STDERR "$progname: file $f is exactly right.\n"; } elsif ($ratio <= 1.0) { print STDERR "$progname: file $f is missing " . sprintf("%.2f%%", $missing_pct) . " ($timestr)\n"; } else { print STDERR "$progname: file $f is larger than expected: " . sprintf("%.2f%%", $ratio * 100) . " ($timestr)\n"; } } $count++; } error "no files to check?" unless ($count > 0); } sub usage() { print STDERR "usage: $progname [--verbose] [--detail] [--check] " . "outfile filenames\n"; exit 1; } sub main() { my @names = (); my $detailp = 0; my $checkp = 0; my $rawp = 0; my $outfile = undef; while ($_ = $ARGV[0]) { shift @ARGV; if ($_ eq "--verbose") { $verbose++; } elsif (m/^-v+$/) { $verbose += length($_)-1; } elsif ($_ eq "--detail") { $detailp++; } elsif ($_ eq "--raw") { $rawp++; } elsif ($_ eq "--check") { $checkp++; } elsif (m/^-./) { usage; } elsif (!defined($outfile)) { $outfile = $_; } else { push @names, $_; } } usage unless defined($outfile); usage unless ($#names >= 0); $detailp = -1 if ($rawp); if ($checkp) { check_files (@names); } else { indexer ($detailp, $outfile, @names); } } main; exit 0;