#!/usr/local/bin/perl -w

# Podcastamatic v1.3
# Kenward Bradley
# http://bradley.chicago.il.us/projects/podcastamatic/
# 2 December 2005

# This program looks at MP3 tags for a Podcast, and generates a webpage and RSS feed (XML file).
# You must have the module MP3::Info installed. Get MP3::Info at http://www.cpan.org/modules/by-module/MP3/

use strict;
use MP3::Info;

#use MP4::Info;


my $MP4INFO = eval{require MP4::Info};

if ($MP4INFO) {
	use MP4::Info;
	print "We have MP4::Info\n";  
	}
	else {
	print "We DONT have MP4::Info\n";  
	}

sub MakeXML;
sub MakeHTML;
sub MakeFromTemplate;
sub BuildEntry;
sub DoTemplateReplacements;
sub logprint;
sub logdie;
sub ecd;
sub iCat;

# don't modify variables here, edit the config file instead
my $DefaultConfigfilename = "/usr/local/etc/podcastamatic.conf";
my $NowTime = TimeString(time);
my $Version = "Podcastamatic v1.2";
my @AudioFiles;
my @FileStats;    
my %Config;

$Config{LogFile} = ""; # preinit this var to avoid warnings
$Config{EscapeCharacterData} = 1; # set default to 1 ( = True) to enable the escape of character data
$Config{iTunesSupport} = 0; # by default iTunes specific tags support if off -- it is an experimental feature

@ARGV = ($DefaultConfigfilename) unless @ARGV;

print "$Version\n\n";

readconfig($ARGV[0]);

if ($Config{LogFile} ne "") { # if we are using a logfile, open it.
	open (LOGFILE, '>', $Config{LogFile}) or logdie "Can't open \"$Config{LogFile}\" for Log output.\n";
	print LOGFILE "$Version\n$NowTime\n\n";
	}
	
my $IndexOfAudioFiles = -1;

logprint "Looking for audio files...\n";

# look for audio files and populate the hash with file and tag data
foreach (glob("$Config{AudioPathServerSide}")) {

	if ((/.mp3$/i) || (/.mp4$/i) || (/.m4a$/i) || (/.m4p$/i)) {
	my $info;
	my $tag;

		$IndexOfAudioFiles++;
		
		@FileStats = stat($_);
		$AudioFiles[$IndexOfAudioFiles]{Filename}     = $_;	
		$AudioFiles[$IndexOfAudioFiles]{Bytes}        = $FileStats[7];
		$AudioFiles[$IndexOfAudioFiles]{Epoc}         = $FileStats[9];   # used for sorting
		$AudioFiles[$IndexOfAudioFiles]{FileModified} = TimeString($FileStats[9]);
		
		logprint "   Found $AudioFiles[$IndexOfAudioFiles]{Filename}\n";
		$AudioFiles[$IndexOfAudioFiles]{Filename} = $_;

			if (/.mp3$/i) {
				$info = get_mp3info($_);  # MP3::Info module required
				$tag  = get_mp3tag($_);   # MP3::Info module required
				$AudioFiles[$IndexOfAudioFiles]{FileType} = "MP3";
				}

			if ($MP4INFO && ((/.mp4$/i) || (/.m4a$/i) || (/.m4p$/i))) {
				$info = get_mp4info($_);  # MP4::Info module required
				$tag  = get_mp4tag($_);   # MP4::Info module required
				$AudioFiles[$IndexOfAudioFiles]{FileType} = "MP4";
				}
			
			# retrieve values from audio files
			$AudioFiles[$IndexOfAudioFiles]{Time}     = sprintf("%02d:%02d", $info->{MM}, $info->{SS});
			$AudioFiles[$IndexOfAudioFiles]{Comment}  = $tag->{COMMENT};
			$AudioFiles[$IndexOfAudioFiles]{Title}    = $tag->{TITLE};
			$AudioFiles[$IndexOfAudioFiles]{Artist}   = $tag->{ARTIST};
			$AudioFiles[$IndexOfAudioFiles]{Album}    = $tag->{ALBUM};
			$AudioFiles[$IndexOfAudioFiles]{Year}     = $tag->{YEAR};
			$AudioFiles[$IndexOfAudioFiles]{Genre}    = $tag->{GENRE};
			
			# If we are configured to Escape Character Data, then fix relevant data
			if ($Config{EscapeCharacterData} == 1) {
				$AudioFiles[$IndexOfAudioFiles]{Comment}  = &ecd($tag->{COMMENT});
				$AudioFiles[$IndexOfAudioFiles]{Title}    = &ecd($tag->{TITLE});
				$AudioFiles[$IndexOfAudioFiles]{Artist}   = &ecd($tag->{ARTIST});
				$AudioFiles[$IndexOfAudioFiles]{Album}    = &ecd($tag->{ALBUM});
				$AudioFiles[$IndexOfAudioFiles]{Genre}    = &ecd($tag->{GENRE});
			}
			# if the title is empty then set it to the filename and pretty it up
			if (not $AudioFiles[$IndexOfAudioFiles]{Title}) {
				my $AudioFileNoPathNoExt = $AudioFiles[$IndexOfAudioFiles]{Filename};
				#this ugly regex removes the path and extension
				$AudioFileNoPathNoExt =~ /^.*\/(.+)\..*$/;
				# escape char data and make it titlecase (usfirst)
				$AudioFiles[$IndexOfAudioFiles]{Title} = &ecd(ucfirst($1)); 
				}
	}
}

if ($IndexOfAudioFiles == -1) {logdie "No audio files found at $Config{AudioPathServerSide} ! Please check your config file.\n";} 

#print how many audio files were found
logprint "   ";
logprint $IndexOfAudioFiles + 1 . " audio files were found.\n";

# Sort Order
# 1 = newest first
# 2 = oldest first
# 3 = alpha order
# 4 = reverse alpha order
if (not defined($Config{SortOrder})) {$Config{SortOrder} = 1;}
# 1 = newest first
if ($Config{SortOrder}==1) {@AudioFiles = sort { $b->{Epoc} <=> $a->{Epoc}  }  @AudioFiles;}
# 2 = oldest first
if ($Config{SortOrder}==2) {@AudioFiles = sort { $a->{Epoc} <=> $b->{Epoc}  }  @AudioFiles;}
# 3 = alpha order
if ($Config{SortOrder}==3) {@AudioFiles = sort {uc($a->{Title}) cmp uc($b->{Title})} @AudioFiles;}
# 4 = reverse alpha order
if ($Config{SortOrder}==4) {@AudioFiles = sort {uc($b->{Title}) cmp uc($a->{Title})} @AudioFiles;}


# If we are configured to Escape Character Data, then fix relevant data
if ($Config{EscapeCharacterData} == 1) {
	$Config{Title}        = &ecd($Config{Title});
	$Config{Description}  = &ecd($Config{Description});
	$Config{Copyright}    = &ecd($Config{Copyright});
}
			
# Make the XML file
if ($Config{MakeXMLFile} == 1)  {MakeXML;} 
	else {logprint "Note: No XML file to be generated per config file.\n";}

# Make the HTML file
if (($Config{MakeHTMLFile} == 1) and ($Config{UseTemplateForHTML} != 1)) {MakeHTML;}
	else {logprint "Note: No automatic HTML file to be generated per config file.\n";}
# Make the HTML file from template

if ($Config{UseTemplateForHTML} == 1) {MakeFromTemplate;} 
	else {logprint "Note: No template driven HTML file to be generated per config file.\n";}

close LOGFILE;


# ---------- Subroutines ----------

sub logprint {
	print $_[0];
	if ($Config{LogFile} ne "") {print LOGFILE $_[0];}
}

sub logdie {
	if ($Config{LogFile} ne "") {print LOGFILE "DIE $_[0]";}
	die "$_[0]";
}

sub MakeHTML {
	logprint "Building automatic HTML file \"$Config{HTMLServerSide}\"\n";
	
	open (HTMLFILE, '>', $Config{HTMLServerSide}) or logdie "Can't open \"$Config{HTMLServerSide}\" for HTML output.\n";
	print HTMLFILE "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\">";
	print HTMLFILE "<HTML>\n<HEAD>\n";
	print HTMLFILE "<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html\">\n";
	print HTMLFILE "<META NAME=\"generator\" CONTENT=\"$Version http://bradley.chicago.il.us/projects/podcastamatic/\">\n";
	print HTMLFILE "<LINK rel=\"stylesheet\" HREF=\"$Config{StylesheetWebSide}\" TYPE=\"text/css\">\n";
	print HTMLFILE "<TITLE>$Config{Title}</TITLE>\n";
	print HTMLFILE "</HEAD>\n<BODY>\n";
	print HTMLFILE DoTemplateReplacements($Config{HeaderHTML});
	print HTMLFILE "\n";
	for my $ca (0..$IndexOfAudioFiles) {
		print HTMLFILE BuildEntry($ca);
		print HTMLFILE "\n";
	}
	
	print HTMLFILE "<P class=\"copyright\">$Config{Copyright}</P>\n";
	# please do not modify the Podcastamatic tagline below
	print HTMLFILE "<P class=\"generatedby\">HTML and XML generated by <A href=\"http://bradley.chicago.il.us/projects/podcastamatic/\">Podcastamatic</A></P>\n";
	print HTMLFILE "<P class=\"generatedtime\">Page built: $NowTime</P>\n";
	# please do not modify the Podcastamatic tagline above
	
	print HTMLFILE DoTemplateReplacements($Config{FooterHTML});
	
	print HTMLFILE "</BODY>\n</HTML>\n";
	
	close HTMLFILE;
	
	logprint "   Automatic HTML file has been created.\n";
}

sub MakeXML {
	
	logprint "Building XML file \"$Config{XMLServerSide}\"\n";
	
	open (XMLFILE, '>', $Config{XMLServerSide}) or logdie "Can't open \"$Config{XMLServerSide}\" for XML output.\n";
	
	print XMLFILE "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
	
	if ($Config{iTunesSupport} == 1) {
		print XMLFILE "<rss xmlns:itunes=\"http://www.itunes.com/DTDs/Podcast-1.0.dtd\" version=\"2.0\">\n";
		}
	else {
		print XMLFILE "<rss version=\"2.0\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n";
		}
	
	print XMLFILE "<channel>\n";
	print XMLFILE "     <title>$Config{Title}</title>\n";
	
	if ($Config{iTunesSupport} == 1) {
		print XMLFILE "<itunes:author>$Config{iTunesAuthor}</itunes:author>\n";
		print XMLFILE "<itunes:link rel=\"image\" type=\"video/jpeg\" ";
		print XMLFILE "href=\"$Config{iTunesLinkJpeg}\">$Config{iTunesLinkText}</itunes:link>\n";

		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 
		if ($Config{iTunesExplicit} eq "yes") {print XMLFILE "<itunes:explicit>yes</itunes:explicit>\n";}
		if ($Config{iTunesExplicit} eq "no") {print XMLFILE "<itunes:explicit>no</itunes:explicit>\n";}

		print XMLFILE $Config{iTunesCategory};
		}
		
	print XMLFILE "     <link>$Config{Link}</link>\n";
	print XMLFILE "     <description>$Config{Description}</description>\n";
	print XMLFILE "     <language>$Config{Language}</language>\n";
	print XMLFILE "     <copyright>$Config{Copyright}</copyright>\n";
	print XMLFILE "     <lastBuildDate>$NowTime</lastBuildDate>\n";
	print XMLFILE "     <ttl>60</ttl>\n";
	
	for my $c (0..$IndexOfAudioFiles) {
		my $AudioFileNoPath = $AudioFiles[$c]{Filename};
		$AudioFileNoPath =~ /^.*\/(.+$)/;
		my $AudioFileWebSide = $Config{AudioPathWebSide} . &urlencode($1);
	
		logprint "   Adding \"$AudioFiles[$c]{Title}\"\n";
		
		print XMLFILE "     <item>\n";
		print XMLFILE "          <title>$AudioFiles[$c]{Title}</title>\n";
		print XMLFILE "          <link>$AudioFileWebSide</link>\n";
		
		if ($Config{iTunesSupport} == 1) {
			print XMLFILE "<itunes:author>$Config{iTunesAuthor}</itunes:author>\n";
			print XMLFILE $Config{iTunesCategory};
			print XMLFILE "<itunes:duration>$AudioFiles[$c]{Time}</itunes:duration>\n";
			if ($Config{iTunesExplicit} eq "yes") {print XMLFILE "<itunes:explicit>yes</itunes:explicit>\n";}
			if ($Config{iTunesExplicit} eq "no") {print XMLFILE "<itunes:explicit>no</itunes:explicit>\n";}

			}
		
# XML description here
		print XMLFILE "          <description>$AudioFiles[$c]{Comment}";
		print XMLFILE " (Running Time $AudioFiles[$c]{Time})</description>\n";
		
		print XMLFILE "          <pubDate>$AudioFiles[$c]{FileModified}</pubDate>\n";
		print XMLFILE "          <enclosure url=\"$AudioFileWebSide\" length=\"$AudioFiles[$c]{Bytes}\" ";

		# assign file type
		if ($AudioFiles[$c]{FileType} eq "MP3") {print XMLFILE "type=\"audio/mpeg\"/>\n";}
		if ($AudioFiles[$c]{FileType} eq "MP4") {print XMLFILE "type=\"audio/x-m4a\"/>\n";}
		
		print XMLFILE "          </item>\n";
	
		last if ($c == ($Config{XMLMaxEntries} -1)); # only do up to the Max Entries
	}
	
	print XMLFILE "	</channel>\n";
	print XMLFILE "</rss>\n";
	close XMLFILE;
	
	logprint "   XML file is done.\n";
}

sub TimeString {

my @t=gmtime($_[0]);
my @DayOfWeek=("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
my @MonthName=("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");

return "$DayOfWeek[$t[6]], $t[3] $MonthName[$t[4]] " . ($t[5]+1900) . sprintf(" %02d:", $t[2]) . sprintf("%02d:", $t[1]) . sprintf("%02d", $t[0]) . " GMT";

#      0    1    2     3     4    5     6     7
#    ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime(time);

# example output "Fri, 31 Dec 2004 17:00:00 GMT"

}

sub readconfig {
	my $configfile = $_[0];
	my $Item;
	my $Value;
	$Config{MoreInfo}   = ""; #initialize to avoid warning
	$Config{AdditionalHTML} = ""; #initialize to avoid warning
	
	open (INPUT, $configfile)         or logdie "Can't open config file: $configfile: $!\n";
	print "Reading configuration file \"$configfile\" ...\n";
	while (<INPUT>) {
		chomp;
			
		s/^\s+//;	               # remove leading whitespace
		s/\s+$//;                  # remove trailing whitespace
	
		if (/^#/ || /^$/) {next}; # ignore comments and blank lines
		/^(\w+)(\s*)(.*)$/;        # parse config items into $1 and $3 ($2 is whitespace)
		$Item = $1;
		$Value = $3;
		
		# check for valid config items, to protect against typos
		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
		
		if ($Item eq "HeaderHTML")            {$Config{$Item} .= $Value . "\n";}  # multiline item
			elsif ($Item eq "FooterHTML")     {$Config{$Item} .= $Value . "\n";}  # multiline item
			elsif ($Item eq "InEachEntry")    {$Config{$Item} .= $Value . "\n";}  # multiline item
			elsif ($Item eq "AdditionalHTML") {$Config{$Item} .= $Value . "\n";}  # multiline, synonym for FooterHTML
			elsif ($Item eq "MoreInfo")       {$Config{$Item} .= $Value . "\n";}	# multiline, synonym for HeaderHTML
			elsif ($Item eq "iTunesCategory") {$Config{$Item} .= &iCat($Value);}	# iTunes Categories
			else {$Config{$Item} = $Value;} 						   	            # default assignment
		}
	
	$Config{HeaderHTML} .= $Config{MoreInfo};				# MoreInfo parm to be phased out
	$Config{FooterHTML} .= $Config{AdditionalHTML};		# AdditionalHTML parm to be phased out

	close(INPUT)                or logdie "can't close $configfile: $!\n";
	
	if ($Config{iTunesSupport} == 1) {print "\nEXPERIMENTAL iTunes specific tag support is ON.\n";}
		else {print "iTunes specific tag support is OFF.\n";}
	
	print " Done.\n";
	}

sub iCat {
#experimental!
#parse iTunes Catagories and format as XML
	my $val = $_[0];
	
	if ($Config{EscapeCharacterData} == 1) {$val = &ecd($val)};
	
	if ($val =~ /,/) {
		$val =~ /^(.+),(.+)$/;
		$val = "<itunes:category text=\"$1\">\n<itunes:category text=\"$2\"/>\n</itunes:category>\n";
	}
	else {$val = "<category>$val</category>\n"}

	return $val;
}

sub urlencode {
# subroutine: urlencode a string (thanks to Brian Hefferan)
       my $url = shift @_;
       #next line lifted from CGI.pm
         $url =~ s/([^a-zA-Z0-9_.%;&?\/\\:+=~-])/sprintf("%%%02X",ord($1))/eg;
         return $url; 
}

sub BuildEntry{
# build individual entry for each MP3 in the HTML page
	my $c = $_[0];
	my $entry = $Config{InEachEntry};
	my $AudioFileNoPath = $AudioFiles[$c]{Filename};
	$AudioFileNoPath =~ /^.*\/(.+$)/;
    my $AudioFileWebSide = $Config{AudioPathWebSide} . &urlencode($1);
	
	logprint "   Adding \"$AudioFiles[$c]{Title}\"\n";

	$entry =~ s/\[TITLE\]/$AudioFiles[$c]{Title}/g;
	$entry =~ s/\[COMMENT\]/$AudioFiles[$c]{Comment}/g;
	$entry =~ s/\[AUDIOFILE\]/$AudioFileWebSide/g;
	$entry =~ s/\[AUDIOFILE_NO_PATH\]/&urlencode($1)/g;
	$entry =~ s/\[AUDIOFILE_NO_PATH_NO_ENCODE\]/$1/g;
	$entry =~ s/\[RUNNING_TIME\]/$AudioFiles[$c]{Time}/g;

	$entry =~ s/\[BYTES\]/$AudioFiles[$c]{Bytes}/g;
	my $kbytes = sprintf("%.0f", $AudioFiles[$c]{Bytes}/1024);
	$entry =~ s/\[KBYTES\]/$kbytes/g;
	my $mbytes = sprintf("%.1f", $AudioFiles[$c]{Bytes}/1024/1024);
	$entry =~ s/\[MBYTES\]/$mbytes/g;
	
	$entry =~ s/\[FILE_MODIFIED\]/$AudioFiles[$c]{FileModified}/g;
	$entry =~ s/\[ARTIST\]/$AudioFiles[$c]{Artist}/g;
	$entry =~ s/\[ALBUM\]/$AudioFiles[$c]{Album}/g;
	$entry =~ s/\[YEAR\]/$AudioFiles[$c]{Year}/g;
	$entry =~ s/\[GENRE\]/$AudioFiles[$c]{Genre}/g;
	$entry =~ s/\[FILETYPE\]/$AudioFiles[$c]{FileType}/g;
	$entry =~ s/\[SPACE\]/ /g; #insert a space
	$entry =~ s/\[EOL\]/\n/g; # insert end of line char(s)
	$entry =~ s/\[NULL\]//g; # does nothing
	return $entry;
}

sub ecd {
# Escape Character Data
# certain special characters need to be replaced with escaped strings
# for valid XML and HTML
my $CharacterData = $_[0];
if (defined($CharacterData)) { 
	$CharacterData =~ s/&/&amp;/g;  # escape ampersand 
	$CharacterData =~ s/</&lt;/g;   # escape less than
	$CharacterData =~ s/>/&gt;/g;   # escape greater than
	$CharacterData =~ s/'/&apos;/g; # escape apostrophe (single quote)
	$CharacterData =~ s/"/&quot;/g; # escape double quote
	}
	else {$CharacterData = "";} # doing this makes the value defined
	return $CharacterData;
	}

sub MakeFromTemplate { 
	my $entries = "";
	logprint "Building HTML file \"$Config{HTMLServerSide}\" from template.\n";
	# slurp template file
    open (TEMPLATEFILE, $Config{TemplateFile}) or logdie "Can't open template file: $Config{TemplateFile}: $!\n";
	undef $/;          
	my $template = <TEMPLATEFILE>;
	close (TEMPLATEFILE) or logdie "Can't close config file: $Config{TemplateFile}: $!\n";
	
	# do replacements
	for my $ca (0..$IndexOfAudioFiles) {	# build the list of entries
		$entries .= BuildEntry($ca);
	}
	$template =~ s/\[ENTRIES\]/$entries/;
	$template = DoTemplateReplacements($template);
	
	# save output
	open (HTMLFILE, '>', $Config{HTMLServerSide}) or logdie "Can't open \"$Config{HTMLServerSide}\" for HTML output.\n";
	print HTMLFILE $template;
	close (HTMLFILE) or logdie "Can't close HTML file: $Config{HTMLServerSide}: $!\n";
	logprint "   HTML file has been created from the template \"$Config{TemplateFile}\".\n";

}

sub DoTemplateReplacements {
	my $template = $_[0];

	$template =~ s/\[HEADERHTML\]/$Config{HeaderHTML}/g;
	$template =~ s/\[FOOTERHTML\]/$Config{FooterHTML}/g;
	$template =~ s/\[BUILDTIME\]/$NowTime/g;
	$template =~ s/\[TITLE\]/$Config{Title}/g;
	$template =~ s/\[DESCRIPTION\]/$Config{Description}/g;
	$template =~ s/\[COPYRIGHT\]/$Config{Copyright}/g;
	$template =~ s/\[MORE_INFO\]/$Config{MoreInfo}/g;
	$template =~ s/\[ADDITIONAL_HTML\]/$Config{AdditionalHTML}/g;
	$template =~ s/\[STYLESHEET_WEBSIDE\]/$Config{StylesheetWebSide}/g;
	$template =~ s/\[XMLWEBSIDE\]/$Config{XMLWebSide}/g;
	$template =~ s/\[SPACE\]/ /g; #insert a space
	$template =~ s/\[EOL\]/\n/g; # insert end of line char(s)
	$template =~ s/\[NULL\]//g; # does nothing
	return $template;
}