#!/usr/bin/perl
# newmusicradio.pl
# Perl CGI script to generate MP3 playlists
# (c) Copyright 2004 Adrian McEwen.  Freely distributable for non-commercial use
# http://www.mcqn.net/projects/NewMusicRadio/
# Version 1.2 July 2004 Delete functionality reworked to use $deleteList
# Version 1.1 July 2004 View playlist page now shows track info and filename
# Version 1.0 July 2004 Initial version

# Installation
# ============
# 1. Install as per a CGI script on your webserver.
# 2. Set $mp3_tag_available, $mp3list, $deleteFilter and $deleteList according
#    to your system
# 3. Point your web-browser at it

# If MP3::Tag is available on your system, set $mp3_tag_available to 1 to get
# Artist/Title/Album info on the "view playlist" page.  Otherwise set it to 0
#$mp3_tag_available = 0; # We don't have MP3::Tag or don't want nice track info
$mp3_tag_available = 1; # We have MP3::Tag, and want nice track info

use CGI qw/:standard/;
use File::Basename;
if ($mp3_tag_available) {
	require MP3::Tag;
}

# Location of a file containing the list of MP3s to randomly choose from
# For example:
# 	find /home/musicfiles -name \*\.mp3 -ctime -7 > recentfiles.txt
# will give you a suitable listing in recentfiles.txt of all the files
# downloaded in the past 7 days
$mp3list = "/home/tunes/recentfiles.txt";
# deleteFilter is a regular expression which will be compared to the full
# path for each track to be used in the playlist.  If it matches, then a
# delete button will be generated in the "view playlist" page, allowing the
# user to delete that MP3.  (it actually replaces the MP3 with one of 0 bytes
# so that wget won't just download it again for you)
# Leaving it as is will allow any file to be added to the deleteList
$deleteFilter = "^.*\$";
# Location of a file which will contain the list of "deleted" files
$deleteList = "/home/tunes/deletedfiles.txt";


MAIN:
{
	$q = new CGI;

	if ( ($q->param('sessionid')) && defined($q->param('track')) ) {
		# This is a request to stream the MP3 for track 
		# <tracknumber>
		$trackname = GetTrackName($q->param('sessionid'),
					  $q->param('track'));
		StreamMP3($q, $trackname);
	}
	elsif ( ($q->param('sessionid')) && ($q->param('trackcount'))
       		&& ($q->param('listen')) ) {
		# Output the generated playlist as an m3u
		OutputM3UPlaylist($q, $q->param('sessionid'), 
				  $q->param('trackcount'));
	}
	elsif ( ($q->param('sessionid')) && ($q->param('trackcount')) ) {
		# This is a request to generate a playlist
		OutputHTMLPlaylist($q, $q->param('sessionid'), 
				   $q->param('trackcount'));
	}
	else {
		# Either the wrong parameters were supplied, or no parameters
		# were supplied, so generate the "Playlist generation form"
		OutputPlaylistForm($q);
	}
}

sub OutputPlaylistForm {
	my ($query) = @_;

	print $query->header();
	print $query->start_html(-title=>'New Music Radio - Generate Playlist',
				 -author=>'nmr@mcqn.net');
	print $query->h1('New Music Radio - Generate Playlist');

	print "<p>The session ID is just seeds the random number generator\n";
	print "so you get the same set of tracks for that session.  To just\n";
	print "generate a random set of tracks it can be left as is.</p>\n";

	print "<p>Hopefully the <i>Number of tracks</i> is self-explanatory.\n";
	print "<i>View playlist</i> generates an HTML page showing you the\n";
	print "tracks which would be chosen, and <i>Listen to playlist</i>\n";
	print "generates a m3u playlist file which should load in your\n";
	print "favourite mp3 player.</p>\n";	

	print $query->start_form(-action=>$query->url(), -method=>"get");

	print "<p>Session ID: ";
	print $query->textfield(-name=>'sessionid',
				-default=>(int (rand 1024)));

	print "\n</p>\n";
	print "<p>Number of tracks: ";
	print $query->textfield(-name=>'trackcount',
				-default=>"20");

	print "\n</p>\n<p>";
	print $query->submit(-name=>'generate_playlist',
			     -value=>'View playlist');
	print " ";
	print $query->submit(-name=>'listen',
			     -value=>'Listen to playlist');

	print $query->end_form();

	print "<hr />\n";
	print "<p><a href='http://www.mcqn.net/projects/NewMusicRadio/'\n";
	print "title='Playlist Generator'>NewMusicRadio</a> &copy; 2004\n";
	print "Adrian McEwen.</p>\n";
	print $query->end_html();
}

sub GeneratePlaylist {
	my ($sessionid, $trackcount) = @_;

	my @playlist;

	for (my $i = 0; $i < $trackcount; $i++) {
		$playlist[$i] = GetTrackName($sessionid, $i);
	}

	return @playlist;
}

sub OutputHTMLPlaylist {
	my ($query, $sessionid, $trackcount) = @_;

	print $query->header();

	print $query->start_html(-title=>"New Music Radio - Playlist $sessionid",
				 -author=>'nmr@mcqn.net');
	print $query->h1("New Music Radio - Playlist $sessionid");
	
	if ($query->param('delete')) {
		# We should "delete" a track.  Actually we'll just truncate it
		# to 0 bytes, so it doesn't get downloaded again by wget
		
		# Check that it exists first, and that it matches the deleteFilter
		$deleteTrack = $query->param('delete');
		if ( (-e $deleteTrack) && (deleteFilter ne "")
		    && ($deleteTrack =~ /$deleteFilter/) ) {
		    # Append this file to the list of deleted files
		    if ( open(DELLIST, ">>", $deleteList) ) {
			    # Prefix the file with the date so we can decide
			    # to trim old entries periodically
			    my (undef, undef, undef, $dd, $mm, $yyyy, undef, undef, undef) = localtime();
			    $yyyy += 1900; # Year counts from 1900
			    $mm++; # Month is 0 based
			    my $output = sprintf("%02d-%02d-%04d %s\n", $dd, $mm, $yyyy, $deleteTrack);

			    # Append it to the list of deleted files
			    print DELLIST "$output";
			    close(DELLIST);
#		    if ( open(TRUNCATED, ">", $deleteTrack) ) {
#			    close(TRUNCATED);
#			    print "<p>Deleted file $deleteTrack</p>\n";
#			    # Regenerate the playlist to reflect deleted file
#			    @playlist = GeneratePlaylist($sessionid, $trackcount);
		    } else {
			    print "<p>Failed to delete $deleteTrack</p>\n";
		    }
		}
	}

	my @playlist = GeneratePlaylist($sessionid, $trackcount);

	# Output button to generate .m3u version of this playlist
	print $query->start_form(-action=>$query->url(), -method=>'get');
	print $query->hidden(-name=>'listen',
			     -value=>'now');
	print $query->hidden(-name=>'sessionid',
			     -value=>$sessionid);
	print $query->hidden(-name=>'trackcount',
			     -value=>$trackcount);
	print $query->submit(-name=>'listen',
			     -value=>'Listen to this playlist');
	print $query->end_form();
	
	# And a button to take us back to the generate playlist page
	print " ";
	print $query->start_form(-action=>$query->url(), -method=>'get');
	print $query->submit(-value=>'Generate new playlist');
	print $query->end_form();

	print $query->start_form(-action=>$query->url(),
				 -method=>'get');
	print $query->hidden(-name=>'sessionid',
			     -value=>$sessionid);
	print $query->hidden(-name=>'trackcount',
			     -value=>$trackcount);
	print "<table>\n";

	my $track;
	my $i =1;
	foreach $track (@playlist) {
		print "<tr>\n";
		print "    <td>$i</td>\n";
		print "    <td>";
		if ((deleteFilter ne "") && ($track =~ /$deleteFilter/)) {
			#print $query->hidden(-name=>'delete',
			#		     -value=>$track);
			#print $query->submit(-value=>'Delete');
			print "<button name='delete' value='$track'";
			print " type='submit'>Delete</button>\n";
		}
		print "</td>\n";
		if ($mp3_tag_available) {
			# Print nicer track info
			my $filename = basename($track);
			my $mp3info = MP3::Tag->new($track);
			# Find any ID3 tags in the file
			$mp3info->get_tags;
			my ($title, undef, $artist, $album, undef, undef, undef) = $mp3info->autoinfo();
			$mp3info->close();
			print "    <td><i>$title</i> by $artist, from $album";
			print "</td><td>$filename</td>\n";
		} else {
			# Just print the filename
			print "    <td>$track</td>\n";
		}
		print "</tr>\n";
		$i++;
	}

	print "</table>\n";
	print $query->end_form();

	print "<hr />\n";
	print "<p><a href='http://www.mcqn.net/projects/NewMusicRadio/'\n";
	print "title='Playlist Generator'>NewMusicRadio</a> &copy; 2004\n";
	print "Adrian McEwen.</p>\n";
	print $query->end_html();
}

sub OutputM3UPlaylist {
	my ($query, $sessionid, $trackcount) = @_;

	print $query->header(-type=>'audio/mpegurl');
	#print $query->header(-type=>'text/plain');

	my $url = $query->url();
	for (my $i=0; $i < $trackcount; $i++) {
		print "$url?sessionid=$sessionid&track=$i\n";
	}
}

# Random number generator, taken from Knuth 3.2.1
sub PseudoRand {
	# Initialize constants
	my ($a, $c, $m) = (4096, 150889, 714025);
	my ($seed, $max, $iterations) = @_;

	my $ret = $seed;
	for (my $i=0; $i <= $iterations; $i++) {
		$ret = ($a * $ret + $c) % $m;
	}

	# $ret is now "randomly" chosen number between 0 and $m
	# We want randomly chosen between 0 and $max
	$ret = int (($ret / $m) * $max);
}

sub GetTrackName {
	my ($sessionid, $track) = @_;

	# Read in the list of MP3s to choose from
	open(MP3LIST, "<", $mp3list); # or GenerateErrorPage("Can't open list");
	my @mp3s = <MP3LIST>;
	close(MP3LIST);
	# Strip all the trailing newlines
	chomp(@mp3s);

	# Read in the list of deleted files too
	open(DELETEDLIST, "<", $deleteList); # or GenerateErrorPage("Can't open deleted file list");
	my @deleted = <DELETEDLIST>;
	close(DELETEDLIST);
	my $delFile;
	my %delList;
	for (my $i=0; $i <= $#deleted; $i++) {
		# Strip off leading date and store the filenames as hash keys
		# so we can just use "exists" to check if files are deleted
		($delFile) = $deleted[$i] =~ /\d\d\-\d\d\-\d\d\d\d (.*)$/;
		$delList{"$delFile"} = 1;
	}

	# Pick one at random
	my $idx = PseudoRand($sessionid, $#mp3s+1, $track);
	
	# Ensure that this file exists, and is of non-zero size
	# Find the first available above this in the list (wrapping if need be)
	$startIdx = $idx;
	$searchedAll = 0;
	while ( (!(-e $mp3s[$idx]) || ( exists($delList{"$mp3s[$idx]"}) )) 
		&& !$searchedAll) {
		$idx++;
		if ($idx == $#mp3s+1) {
			$idx = 0;
		}
		if ($idx == $startIdx) {
			$searchedAll = 1;
		}
	}
	return $mp3s[$idx];
}

sub StreamMP3 {
	my ($query, $file) = @_;

	# Open it for input (in binary mode)
	print $file;
	open(TUNE, "<", $file); # or GenerateErrorPage("Can't open $file");
	binmode(TUNE, ":raw");

	# First get the size of the file so we can generate the correct 
	# content-length header
	my $tunesize = (stat(TUNE))[7];

	# Output the HTTP header
	#print $q->header(-type=>'text/plain');
	#print $q->header(-type=>'audio/mpeg',-Content_length=>$tunesize);
	print $query->header(-type=>'audio/x-mp3',-Content_length=>$tunesize);

	# Now just stream the file out
	#print $file;

	# Ensure STDOUT is in binary mode too...
	binmode STDOUT, ":raw";

	my $buf;
	while (read(TUNE, $buf, 32768)) {
		print STDOUT $buf;
	}

	close(TUNE);
}

