mp3info.pl


Discussion

This script exists to (a) scratch an immediate itch and (b) experiment with a few modules. No claim is made to best usage of any of these features! The Itch was scratched, and pudding was proofed.

The Itch was that in moving old podcast files off a modest M.2 SSD to archival spinning rust drive (which is normally kept Read-only mounted for safety), I did not want to rely on Rhythmbox or other music/pod player being sane about browsing/playing a read-only drive where it couldn't mark things PLAYED. I did need to move most of my older mp3, ogg, etc collection that wasn't "in queue" to larger passive storage to make space. But i didn't want to lose the ability to search by track title etc, which usually aren't fully encoded in filenames. As i expected, there's a CPAN module for accessing mp3 etc meta-data tags. So it should be trivial to recursively write HTML indices for directories containing MP3s, with links that will play a file, right?

Desired Output

Directory HTML pages consisting of listings for sudirectories

<br /><div class=subdir><a href="./Following%20On%20in%20India%20Cricket%20Podcast/directory.html">/media/wdr/bulkstorage/wdr/Music/Following On in India Cricket Podcast/</a></div>

which displays as


/media/wdr/bulkstorage/wdr/Music/Following On in India Cricket Podcast/

and for MP3, OGG, etc audio files contained in the directory, emit

<div class=program> <!-- /media/wdr/bulkstorage/wdr/Music/Wynalda%20for%20the%20Win/BLU5671872590.mp3 -->
<br /><span class=attr>Song:	 The USA vs. Mexico Preview Show</span>
<br /><span class=attr>Album:	 Wynalda for the Win</span>
<br /><span class=attr>size: 66M</span>
<br /><span class=attr>date: 2021-11-27</span>
<br /><a href="BLU5671872590.mp3">PLAY</a>
</div>

which renders as


Song: The USA vs. Mexico Preview Show
Album: Wynalda for the Win
size: 66M
date: 2021-11-27
PLAY

(No, those files aren't on the server, so clicking those blue links is pointless.)

Other itches were experimenting with Syntax::Keyword::Try and indented HEREDOCs. Use of try catch is why the (ab)use of redo OPEN instead of testing directory writability with File::stat like a sane person. This overly complex idiom is appropriate for situations where the problem is not foreseeable; this problem is, but i wsa scouting code>Syntax::Keyword::Try for a much larger usecase elsewhere.

Randal (merlyn) and Jerrad (belg4mit, pthbb) made some useful observations for anyone interested in modifying this:

All of these are valid criticism. But it was a one-afternoon minimum-viable itch-scratch that made my space-clearing "safe". It suffices for its intended purposed. Any reuse should apply the above observations!


   1 #!/usr/bin/perl -w
   2 #
   3 # USAGE
   4 #
   5 # perl mp3info.pl $PODS/'Podcast Name In Quotes'
   6 #
   7 # MUSIC_PATH=$HOME/Music
   8 # MUSIC_PATH=/media/$USER/bulkstorage/$USER/Music
   9 # MUSIC_PATH=$HOME/Music/Podcasts
  10 # find $MUSIC_PATH  -type d -print0 | xargs -0 perl ./mp3info.pl
  11 #
  12 # the -print0 and -0 are necessary since Album dirs have spaces :-/
  13 #
  14 # NB: for R/O bulk archive drive mentioned, need to 
  15 #    sudo mount -o remount,rw $mount_point  # before
  16 #    sudo mount -o remount,ro $mount_point  # after 
  17                
  18 use MP3::Tag;
  19 # Mutated from MP#::Tag demo program into tool to generate an
  20 # HTML Directory listings of MP3 etc 
  21 # in new form, instead of a list of files on STDIN, 
  22 # it will take a list of directories on ARGV,
  23 # and write a directory.hml into the directory
  24 
  25 # BUGS
  26 # has same tags for HTML as MP3; can't answer "is it really audio",
  27 # so will likely catalog nohup.out or something !!
  28 
  29 # original example comments
  30 # define how autoinfo tries to get information
  31 # default:
  32 # MP3::Tag->config("autoinfo","ID3v2","ID3v1","filename");
  33 # don't use ID3v2:
  34 # MP3::Tag->config("autoinfo","ID3v1","filename");
  35 
  36 use v5.030;
  37 
  38 use URI::Encode qw(uri_encode uri_decode);
  39 use POSIX qw/strftime/;
  40 use Path::Tiny;
  41 use File::stat;
  42 use Readonly;
  43 use Syntax::Keyword::Try qw( try :experimental );
  44 
  45 Readonly my $page_filename => 'directory.html';
  46 
  47 # @TODO probably need some UTF declarations
  48 # @TODO add link to a vanilla CSS file ? 
  49 # @TODO do we read css for bulk Music from $HOME/Music ?
  50 # @TODO should sort files by either name or f/s date or better Tags date (and maybe dirs first?)
  51 
  52 DIRECTORY:
  53 for my $dir_arg (@ARGV){
  54 
  55     warn ">> entering $dir_arg";
  56 
  57     my $dir = path($dir_arg);
  58 
  59     die "$dir_arg not a directory" unless $dir->is_dir ;
  60 
  61     my @Files = $dir->children;
  62     # warn '>>>>', join(q(,), map{"'$_'"} @Files); # DEBUG
  63 
  64     ## open directory.html FH here
  65     my $fh;
  66     my $dodge=q();
  67 
  68     OPEN:
  69     while ( ! $fh ){
  70         try {
  71              $dir->chmod('ug+w') if $dodge eq 'dir_perm';  # Very Temporary
  72 
  73              $fh = $dir->child($page_filename)->filehandle('>', ':raw:utf8_strict');
  74 
  75              $dir->chmod('ug-w') if $dodge eq 'dir_perm'; # I said Very Temporary!
  76          }
  77          catch ($e =~ m{Error open.*Permission denied}i ) {
  78              warn "caught '$e'; workaround ...";
  79              if ($dodge){
  80                  warn "already dodged $dodge workaround, skipping '$dir'";
  81                  next DIRECTORY;
  82              }
  83              $dodge = 'dir_perm'; 
  84              redo OPEN;
  85              }
  86          catch ($e) {
  87              warn "Unexpected exception caught '$e', exiting $dir";
  88              next DIRECTORY;
  89          }
  90      }
  91 
  92 
  93 
  94     say $fh <<~"EOF";
  95     <html>
  96     <head>
  97     <title>Directory of $dir Audio Files</title>
  98     </head>
  99     <body>
 100     EOF
 101 
 102     FILE:
 103     for my $file (grep { $_->stringify !~ m/[.]htm[l]?$/ } @Files){
 104             if ($file->is_dir){
 105                 warn $file->stringify, " is directory, not recursing, do it separately after";
 106                 say $fh sprintf '<br /><div class=subdir><a href="./%s/%s">%s/</a></div><br />', uri_encode($file->basename), uri_encode($page_filename), $file;
 107                 next;
 108             }
 109             if (my $mp3=MP3::Tag->new($file->stringify)) {
 110                 say $fh sprintf '<div class=program> <!-- %s -->', uri_encode $file->stringify;
 111                 # my @tags = $mp3->get_tags(); 
 112                 # say $fh sprintf '<!-- %s -->', join q(,), map{uri_encode $_} @tags;
 113                 my $stat= $file->stat;
 114                 # debug:
 115                 # print "$file (Tags: ", join(", ",$mp3->get_tags),")\n";
 116                 my %Info;
 117                 my @keys = qw/Song Track Artist Album Comment/ ;
 118                 (@Info{@keys})=$mp3->autoinfo;
 119                 for my $k (@keys){
 120                     # say $fh "<!-- ", $k, q(:), $Info{$k}, "-->";  # Debug
 121                     say $fh "<br /><span class=attr>$k:\t $Info{$k}</span>" if $Info{$k};
 122                 }
 123                 say $fh sprintf "<br /><span class=attr>size: %dM</span>\n<br /><span class=attr>date: %s</span>", int($stat->size/(1<<20)), POSIX::strftime "%F", localtime($stat->mtime)  ;
 124                 say $fh sprintf '<br /><a href="%s">PLAY</a>', uri_encode $file->relative($file->parent);
 125                 say $fh "</div>\n";
 126             } # end if mp3
 127         } # end for files 
 128         say $fh <<~'EOF';
 129         </body>
 130         </html>
 131         EOF
 132 
 133         # close directory.html FH here
 134 
 135 } # end dirs
 136 
 137 
 138 __END__
 139 
 140 Diagnostics
 141 
 142 
 143 >> entering /media/wdr/bulkstorage/wdr/Music/Science Unscripted - Daily news on COVID-19 at ./mp3info.pl line 54.
 144 Ridiculously large tag size: 1945578; file size 920474 at .../lib/perl5/MP3/Tag/ID3v2.pm line 2081.
 145