1#!/usr/local/bin/perl -w 2 3# Podcastamatic v1.3 4# Kenward Bradley 5# http://bradley.chicago.il.us/projects/podcastamatic/ 6# 2 December 2005 7 8# This program looks at MP3 tags for a Podcast, and generates a webpage and RSS feed (XML file). 9# You must have the module MP3::Info installed. Get MP3::Info at http://www.cpan.org/modules/by-module/MP3/ 10 11use strict; 12use MP3::Info; 13 14#use MP4::Info; 15 16 17my $MP4INFO = eval{require MP4::Info}; 18 19if ($MP4INFO) { 20 use MP4::Info; 21 print "We have MP4::Info\n"; 22 } 23 else { 24 print "We DONT have MP4::Info\n"; 25 } 26 27sub MakeXML; 28sub MakeHTML; 29sub MakeFromTemplate; 30sub BuildEntry; 31sub DoTemplateReplacements; 32sub logprint; 33sub logdie; 34sub ecd; 35sub iCat; 36 37# don't modify variables here, edit the config file instead 38my $DefaultConfigfilename = "/usr/local/etc/podcastamatic.conf"; 39my $NowTime = TimeString(time); 40my $Version = "Podcastamatic v1.2"; 41my @AudioFiles; 42my @FileStats; 43my %Config; 44 45$Config{LogFile} = ""; # preinit this var to avoid warnings 46$Config{EscapeCharacterData} = 1; # set default to 1 ( = True) to enable the escape of character data 47$Config{iTunesSupport} = 0; # by default iTunes specific tags support if off -- it is an experimental feature 48 49@ARGV = ($DefaultConfigfilename) unless @ARGV; 50 51print "$Version\n\n"; 52 53readconfig($ARGV[0]); 54 55if ($Config{LogFile} ne "") { # if we are using a logfile, open it. 56 open (LOGFILE, '>', $Config{LogFile}) or logdie "Can't open \"$Config{LogFile}\" for Log output.\n"; 57 print LOGFILE "$Version\n$NowTime\n\n"; 58 } 59 60my $IndexOfAudioFiles = -1; 61 62logprint "Looking for audio files...\n"; 63 64# look for audio files and populate the hash with file and tag data 65foreach (glob("$Config{AudioPathServerSide}")) { 66 67 if ((/.mp3$/i) || (/.mp4$/i) || (/.m4a$/i) || (/.m4p$/i)) { 68 my $info; 69 my $tag; 70 71 $IndexOfAudioFiles++; 72 73 @FileStats = stat($_); 74 $AudioFiles[$IndexOfAudioFiles]{Filename} = $_; 75 $AudioFiles[$IndexOfAudioFiles]{Bytes} = $FileStats[7]; 76 $AudioFiles[$IndexOfAudioFiles]{Epoc} = $FileStats[9]; # used for sorting 77 $AudioFiles[$IndexOfAudioFiles]{FileModified} = TimeString($FileStats[9]); 78 79 logprint " Found $AudioFiles[$IndexOfAudioFiles]{Filename}\n"; 80 $AudioFiles[$IndexOfAudioFiles]{Filename} = $_; 81 82 if (/.mp3$/i) { 83 $info = get_mp3info($_); # MP3::Info module required 84 $tag = get_mp3tag($_); # MP3::Info module required 85 $AudioFiles[$IndexOfAudioFiles]{FileType} = "MP3"; 86 } 87 88 if ($MP4INFO && ((/.mp4$/i) || (/.m4a$/i) || (/.m4p$/i))) { 89 $info = get_mp4info($_); # MP4::Info module required 90 $tag = get_mp4tag($_); # MP4::Info module required 91 $AudioFiles[$IndexOfAudioFiles]{FileType} = "MP4"; 92 } 93 94 # retrieve values from audio files 95 $AudioFiles[$IndexOfAudioFiles]{Time} = sprintf("%02d:%02d", $info->{MM}, $info->{SS}); 96 $AudioFiles[$IndexOfAudioFiles]{Comment} = $tag->{COMMENT}; 97 $AudioFiles[$IndexOfAudioFiles]{Title} = $tag->{TITLE}; 98 $AudioFiles[$IndexOfAudioFiles]{Artist} = $tag->{ARTIST}; 99 $AudioFiles[$IndexOfAudioFiles]{Album} = $tag->{ALBUM}; 100 $AudioFiles[$IndexOfAudioFiles]{Year} = $tag->{YEAR}; 101 $AudioFiles[$IndexOfAudioFiles]{Genre} = $tag->{GENRE}; 102 103 # If we are configured to Escape Character Data, then fix relevant data 104 if ($Config{EscapeCharacterData} == 1) { 105 $AudioFiles[$IndexOfAudioFiles]{Comment} = &ecd($tag->{COMMENT}); 106 $AudioFiles[$IndexOfAudioFiles]{Title} = &ecd($tag->{TITLE}); 107 $AudioFiles[$IndexOfAudioFiles]{Artist} = &ecd($tag->{ARTIST}); 108 $AudioFiles[$IndexOfAudioFiles]{Album} = &ecd($tag->{ALBUM}); 109 $AudioFiles[$IndexOfAudioFiles]{Genre} = &ecd($tag->{GENRE}); 110 } 111 # if the title is empty then set it to the filename and pretty it up 112 if (not $AudioFiles[$IndexOfAudioFiles]{Title}) { 113 my $AudioFileNoPathNoExt = $AudioFiles[$IndexOfAudioFiles]{Filename}; 114 #this ugly regex removes the path and extension 115 $AudioFileNoPathNoExt =~ /^.*\/(.+)\..*$/; 116 # escape char data and make it titlecase (usfirst) 117 $AudioFiles[$IndexOfAudioFiles]{Title} = &ecd(ucfirst($1)); 118 } 119 } 120} 121 122if ($IndexOfAudioFiles == -1) {logdie "No audio files found at $Config{AudioPathServerSide} ! Please check your config file.\n";} 123 124#print how many audio files were found 125logprint " "; 126logprint $IndexOfAudioFiles + 1 . " audio files were found.\n"; 127 128# Sort Order 129# 1 = newest first 130# 2 = oldest first 131# 3 = alpha order 132# 4 = reverse alpha order 133if (not defined($Config{SortOrder})) {$Config{SortOrder} = 1;} 134# 1 = newest first 135if ($Config{SortOrder}==1) {@AudioFiles = sort { $b->{Epoc} <=> $a->{Epoc} } @AudioFiles;} 136# 2 = oldest first 137if ($Config{SortOrder}==2) {@AudioFiles = sort { $a->{Epoc} <=> $b->{Epoc} } @AudioFiles;} 138# 3 = alpha order 139if ($Config{SortOrder}==3) {@AudioFiles = sort {uc($a->{Title}) cmp uc($b->{Title})} @AudioFiles;} 140# 4 = reverse alpha order 141if ($Config{SortOrder}==4) {@AudioFiles = sort {uc($b->{Title}) cmp uc($a->{Title})} @AudioFiles;} 142 143 144# If we are configured to Escape Character Data, then fix relevant data 145if ($Config{EscapeCharacterData} == 1) { 146 $Config{Title} = &ecd($Config{Title}); 147 $Config{Description} = &ecd($Config{Description}); 148 $Config{Copyright} = &ecd($Config{Copyright}); 149} 150 151# Make the XML file 152if ($Config{MakeXMLFile} == 1) {MakeXML;} 153 else {logprint "Note: No XML file to be generated per config file.\n";} 154 155# Make the HTML file 156if (($Config{MakeHTMLFile} == 1) and ($Config{UseTemplateForHTML} != 1)) {MakeHTML;} 157 else {logprint "Note: No automatic HTML file to be generated per config file.\n";} 158# Make the HTML file from template 159 160if ($Config{UseTemplateForHTML} == 1) {MakeFromTemplate;} 161 else {logprint "Note: No template driven HTML file to be generated per config file.\n";} 162 163close LOGFILE; 164 165 166# ---------- Subroutines ---------- 167 168sub logprint { 169 print $_[0]; 170 if ($Config{LogFile} ne "") {print LOGFILE $_[0];} 171} 172 173sub logdie { 174 if ($Config{LogFile} ne "") {print LOGFILE "DIE $_[0]";} 175 die "$_[0]"; 176} 177 178sub MakeHTML { 179 logprint "Building automatic HTML file \"$Config{HTMLServerSide}\"\n"; 180 181 open (HTMLFILE, '>', $Config{HTMLServerSide}) or logdie "Can't open \"$Config{HTMLServerSide}\" for HTML output.\n"; 182 print HTMLFILE "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\">"; 183 print HTMLFILE "<HTML>\n<HEAD>\n"; 184 print HTMLFILE "<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html\">\n"; 185 print HTMLFILE "<META NAME=\"generator\" CONTENT=\"$Version http://bradley.chicago.il.us/projects/podcastamatic/\">\n"; 186 print HTMLFILE "<LINK rel=\"stylesheet\" HREF=\"$Config{StylesheetWebSide}\" TYPE=\"text/css\">\n"; 187 print HTMLFILE "<TITLE>$Config{Title}</TITLE>\n"; 188 print HTMLFILE "</HEAD>\n<BODY>\n"; 189 print HTMLFILE DoTemplateReplacements($Config{HeaderHTML}); 190 print HTMLFILE "\n"; 191 for my $ca (0..$IndexOfAudioFiles) { 192 print HTMLFILE BuildEntry($ca); 193 print HTMLFILE "\n"; 194 } 195 196 print HTMLFILE "<P class=\"copyright\">$Config{Copyright}</P>\n"; 197 # please do not modify the Podcastamatic tagline below 198 print HTMLFILE "<P class=\"generatedby\">HTML and XML generated by <A href=\"http://bradley.chicago.il.us/projects/podcastamatic/\">Podcastamatic</A></P>\n"; 199 print HTMLFILE "<P class=\"generatedtime\">Page built: $NowTime</P>\n"; 200 # please do not modify the Podcastamatic tagline above 201 202 print HTMLFILE DoTemplateReplacements($Config{FooterHTML}); 203 204 print HTMLFILE "</BODY>\n</HTML>\n"; 205 206 close HTMLFILE; 207 208 logprint " Automatic HTML file has been created.\n"; 209} 210 211sub MakeXML { 212 213 logprint "Building XML file \"$Config{XMLServerSide}\"\n"; 214 215 open (XMLFILE, '>', $Config{XMLServerSide}) or logdie "Can't open \"$Config{XMLServerSide}\" for XML output.\n"; 216 217 print XMLFILE "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"; 218 219 if ($Config{iTunesSupport} == 1) { 220 print XMLFILE "<rss xmlns:itunes=\"http://www.itunes.com/DTDs/Podcast-1.0.dtd\" version=\"2.0\">\n"; 221 } 222 else { 223 print XMLFILE "<rss version=\"2.0\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n"; 224 } 225 226 print XMLFILE "<channel>\n"; 227 print XMLFILE " <title>$Config{Title}</title>\n"; 228 229 if ($Config{iTunesSupport} == 1) { 230 print XMLFILE "<itunes:author>$Config{iTunesAuthor}</itunes:author>\n"; 231 print XMLFILE "<itunes:link rel=\"image\" type=\"video/jpeg\" "; 232 print XMLFILE "href=\"$Config{iTunesLinkJpeg}\">$Config{iTunesLinkText}</itunes:link>\n"; 233 234 if ($Config{iTunesExplicit} !~ m/^(yes|no|undefined)$/) {logdie("iTunesExplicit is not properly defined. If you are using iTunes support then you need to configure iTunesExplicit. Please see the documentation.\n");} # error 235 if ($Config{iTunesExplicit} eq "yes") {print XMLFILE "<itunes:explicit>yes</itunes:explicit>\n";} 236 if ($Config{iTunesExplicit} eq "no") {print XMLFILE "<itunes:explicit>no</itunes:explicit>\n";} 237 238 print XMLFILE $Config{iTunesCategory}; 239 } 240 241 print XMLFILE " <link>$Config{Link}</link>\n"; 242 print XMLFILE " <description>$Config{Description}</description>\n"; 243 print XMLFILE " <language>$Config{Language}</language>\n"; 244 print XMLFILE " <copyright>$Config{Copyright}</copyright>\n"; 245 print XMLFILE " <lastBuildDate>$NowTime</lastBuildDate>\n"; 246 print XMLFILE " <ttl>60</ttl>\n"; 247 248 for my $c (0..$IndexOfAudioFiles) { 249 my $AudioFileNoPath = $AudioFiles[$c]{Filename}; 250 $AudioFileNoPath =~ /^.*\/(.+$)/; 251 my $AudioFileWebSide = $Config{AudioPathWebSide} . &urlencode($1); 252 253 logprint " Adding \"$AudioFiles[$c]{Title}\"\n"; 254 255 print XMLFILE " <item>\n"; 256 print XMLFILE " <title>$AudioFiles[$c]{Title}</title>\n"; 257 print XMLFILE " <link>$AudioFileWebSide</link>\n"; 258 259 if ($Config{iTunesSupport} == 1) { 260 print XMLFILE "<itunes:author>$Config{iTunesAuthor}</itunes:author>\n"; 261 print XMLFILE $Config{iTunesCategory}; 262 print XMLFILE "<itunes:duration>$AudioFiles[$c]{Time}</itunes:duration>\n"; 263 if ($Config{iTunesExplicit} eq "yes") {print XMLFILE "<itunes:explicit>yes</itunes:explicit>\n";} 264 if ($Config{iTunesExplicit} eq "no") {print XMLFILE "<itunes:explicit>no</itunes:explicit>\n";} 265 266 } 267 268# XML description here 269 print XMLFILE " <description>$AudioFiles[$c]{Comment}"; 270 print XMLFILE " (Running Time $AudioFiles[$c]{Time})</description>\n"; 271 272 print XMLFILE " <pubDate>$AudioFiles[$c]{FileModified}</pubDate>\n"; 273 print XMLFILE " <enclosure url=\"$AudioFileWebSide\" length=\"$AudioFiles[$c]{Bytes}\" "; 274 275 # assign file type 276 if ($AudioFiles[$c]{FileType} eq "MP3") {print XMLFILE "type=\"audio/mpeg\"/>\n";} 277 if ($AudioFiles[$c]{FileType} eq "MP4") {print XMLFILE "type=\"audio/x-m4a\"/>\n";} 278 279 print XMLFILE " </item>\n"; 280 281 last if ($c == ($Config{XMLMaxEntries} -1)); # only do up to the Max Entries 282 } 283 284 print XMLFILE " </channel>\n"; 285 print XMLFILE "</rss>\n"; 286 close XMLFILE; 287 288 logprint " XML file is done.\n"; 289} 290 291sub TimeString { 292 293my @t=gmtime($_[0]); 294my @DayOfWeek=("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"); 295my @MonthName=("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"); 296 297return "$DayOfWeek[$t[6]], $t[3] $MonthName[$t[4]] " . ($t[5]+1900) . sprintf(" %02d:", $t[2]) . sprintf("%02d:", $t[1]) . sprintf("%02d", $t[0]) . " GMT"; 298 299# 0 1 2 3 4 5 6 7 300# ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime(time); 301 302# example output "Fri, 31 Dec 2004 17:00:00 GMT" 303 304} 305 306sub readconfig { 307 my $configfile = $_[0]; 308 my $Item; 309 my $Value; 310 $Config{MoreInfo} = ""; #initialize to avoid warning 311 $Config{AdditionalHTML} = ""; #initialize to avoid warning 312 313 open (INPUT, $configfile) or logdie "Can't open config file: $configfile: $!\n"; 314 print "Reading configuration file \"$configfile\" ...\n"; 315 while (<INPUT>) { 316 chomp; 317 318 s/^\s+//; # remove leading whitespace 319 s/\s+$//; # remove trailing whitespace 320 321 if (/^#/ || /^$/) {next}; # ignore comments and blank lines 322 /^(\w+)(\s*)(.*)$/; # parse config items into $1 and $3 ($2 is whitespace) 323 $Item = $1; 324 $Value = $3; 325 326 # check for valid config items, to protect against typos 327 if ($Item !~ m/^(Title|SortOrder|MakeHTMLFile|MakeXMLFile|UseTemplateForHTML|TemplateFile|LogFile|Link|Description|Language|Copyright|MoreInfo|AdditionalHTML|InEachEntry|AudioPathServerSide|AudioPathWebSide|HTMLServerSide|StylesheetWebSide|XMLServerSide|XMLWebSide|XMLMaxEntries|HeaderHTML|FooterHTML|iTunesSupport|iTunesAuthor|iTunesExplicit|iTunesLinkJpeg|iTunesLinkText|iTunesCategory)$/) {logdie("Error in config file, please fix it. I don\'t understand this:\n\"$_\"\n");} # error unknown data 328 329 if ($Item eq "HeaderHTML") {$Config{$Item} .= $Value . "\n";} # multiline item 330 elsif ($Item eq "FooterHTML") {$Config{$Item} .= $Value . "\n";} # multiline item 331 elsif ($Item eq "InEachEntry") {$Config{$Item} .= $Value . "\n";} # multiline item 332 elsif ($Item eq "AdditionalHTML") {$Config{$Item} .= $Value . "\n";} # multiline, synonym for FooterHTML 333 elsif ($Item eq "MoreInfo") {$Config{$Item} .= $Value . "\n";} # multiline, synonym for HeaderHTML 334 elsif ($Item eq "iTunesCategory") {$Config{$Item} .= &iCat($Value);} # iTunes Categories 335 else {$Config{$Item} = $Value;} # default assignment 336 } 337 338 $Config{HeaderHTML} .= $Config{MoreInfo}; # MoreInfo parm to be phased out 339 $Config{FooterHTML} .= $Config{AdditionalHTML}; # AdditionalHTML parm to be phased out 340 341 close(INPUT) or logdie "can't close $configfile: $!\n"; 342 343 if ($Config{iTunesSupport} == 1) {print "\nEXPERIMENTAL iTunes specific tag support is ON.\n";} 344 else {print "iTunes specific tag support is OFF.\n";} 345 346 print " Done.\n"; 347 } 348 349sub iCat { 350#experimental! 351#parse iTunes Catagories and format as XML 352 my $val = $_[0]; 353 354 if ($Config{EscapeCharacterData} == 1) {$val = &ecd($val)}; 355 356 if ($val =~ /,/) { 357 $val =~ /^(.+),(.+)$/; 358 $val = "<itunes:category text=\"$1\">\n<itunes:category text=\"$2\"/>\n</itunes:category>\n"; 359 } 360 else {$val = "<category>$val</category>\n"} 361 362 return $val; 363} 364 365sub urlencode { 366# subroutine: urlencode a string (thanks to Brian Hefferan) 367 my $url = shift @_; 368 #next line lifted from CGI.pm 369 $url =~ s/([^a-zA-Z0-9_.%;&?\/\\:+=~-])/sprintf("%%%02X",ord($1))/eg; 370 return $url; 371} 372 373sub BuildEntry{ 374# build individual entry for each MP3 in the HTML page 375 my $c = $_[0]; 376 my $entry = $Config{InEachEntry}; 377 my $AudioFileNoPath = $AudioFiles[$c]{Filename}; 378 $AudioFileNoPath =~ /^.*\/(.+$)/; 379 my $AudioFileWebSide = $Config{AudioPathWebSide} . &urlencode($1); 380 381 logprint " Adding \"$AudioFiles[$c]{Title}\"\n"; 382 383 $entry =~ s/\[TITLE\]/$AudioFiles[$c]{Title}/g; 384 $entry =~ s/\[COMMENT\]/$AudioFiles[$c]{Comment}/g; 385 $entry =~ s/\[AUDIOFILE\]/$AudioFileWebSide/g; 386 $entry =~ s/\[AUDIOFILE_NO_PATH\]/&urlencode($1)/g; 387 $entry =~ s/\[AUDIOFILE_NO_PATH_NO_ENCODE\]/$1/g; 388 $entry =~ s/\[RUNNING_TIME\]/$AudioFiles[$c]{Time}/g; 389 390 $entry =~ s/\[BYTES\]/$AudioFiles[$c]{Bytes}/g; 391 my $kbytes = sprintf("%.0f", $AudioFiles[$c]{Bytes}/1024); 392 $entry =~ s/\[KBYTES\]/$kbytes/g; 393 my $mbytes = sprintf("%.1f", $AudioFiles[$c]{Bytes}/1024/1024); 394 $entry =~ s/\[MBYTES\]/$mbytes/g; 395 396 $entry =~ s/\[FILE_MODIFIED\]/$AudioFiles[$c]{FileModified}/g; 397 $entry =~ s/\[ARTIST\]/$AudioFiles[$c]{Artist}/g; 398 $entry =~ s/\[ALBUM\]/$AudioFiles[$c]{Album}/g; 399 $entry =~ s/\[YEAR\]/$AudioFiles[$c]{Year}/g; 400 $entry =~ s/\[GENRE\]/$AudioFiles[$c]{Genre}/g; 401 $entry =~ s/\[FILETYPE\]/$AudioFiles[$c]{FileType}/g; 402 $entry =~ s/\[SPACE\]/ /g; #insert a space 403 $entry =~ s/\[EOL\]/\n/g; # insert end of line char(s) 404 $entry =~ s/\[NULL\]//g; # does nothing 405 return $entry; 406} 407 408sub ecd { 409# Escape Character Data 410# certain special characters need to be replaced with escaped strings 411# for valid XML and HTML 412my $CharacterData = $_[0]; 413if (defined($CharacterData)) { 414 $CharacterData =~ s/&/&/g; # escape ampersand 415 $CharacterData =~ s/</</g; # escape less than 416 $CharacterData =~ s/>/>/g; # escape greater than 417 $CharacterData =~ s/'/'/g; # escape apostrophe (single quote) 418 $CharacterData =~ s/"/"/g; # escape double quote 419 } 420 else {$CharacterData = "";} # doing this makes the value defined 421 return $CharacterData; 422 } 423 424sub MakeFromTemplate { 425 my $entries = ""; 426 logprint "Building HTML file \"$Config{HTMLServerSide}\" from template.\n"; 427 # slurp template file 428 open (TEMPLATEFILE, $Config{TemplateFile}) or logdie "Can't open template file: $Config{TemplateFile}: $!\n"; 429 undef $/; 430 my $template = <TEMPLATEFILE>; 431 close (TEMPLATEFILE) or logdie "Can't close config file: $Config{TemplateFile}: $!\n"; 432 433 # do replacements 434 for my $ca (0..$IndexOfAudioFiles) { # build the list of entries 435 $entries .= BuildEntry($ca); 436 } 437 $template =~ s/\[ENTRIES\]/$entries/; 438 $template = DoTemplateReplacements($template); 439 440 # save output 441 open (HTMLFILE, '>', $Config{HTMLServerSide}) or logdie "Can't open \"$Config{HTMLServerSide}\" for HTML output.\n"; 442 print HTMLFILE $template; 443 close (HTMLFILE) or logdie "Can't close HTML file: $Config{HTMLServerSide}: $!\n"; 444 logprint " HTML file has been created from the template \"$Config{TemplateFile}\".\n"; 445 446} 447 448sub DoTemplateReplacements { 449 my $template = $_[0]; 450 451 $template =~ s/\[HEADERHTML\]/$Config{HeaderHTML}/g; 452 $template =~ s/\[FOOTERHTML\]/$Config{FooterHTML}/g; 453 $template =~ s/\[BUILDTIME\]/$NowTime/g; 454 $template =~ s/\[TITLE\]/$Config{Title}/g; 455 $template =~ s/\[DESCRIPTION\]/$Config{Description}/g; 456 $template =~ s/\[COPYRIGHT\]/$Config{Copyright}/g; 457 $template =~ s/\[MORE_INFO\]/$Config{MoreInfo}/g; 458 $template =~ s/\[ADDITIONAL_HTML\]/$Config{AdditionalHTML}/g; 459 $template =~ s/\[STYLESHEET_WEBSIDE\]/$Config{StylesheetWebSide}/g; 460 $template =~ s/\[XMLWEBSIDE\]/$Config{XMLWebSide}/g; 461 $template =~ s/\[SPACE\]/ /g; #insert a space 462 $template =~ s/\[EOL\]/\n/g; # insert end of line char(s) 463 $template =~ s/\[NULL\]//g; # does nothing 464 return $template; 465}