#!/usr/bin/perl
# Play the data on STDIN as an audio file
#
# $Id: gutenbach-filter,v 1.26 2009/02/20 00:27:17 geofft Exp root $
# $Source: /usr/local/bin/RCS/gutenbach-filter,v $
#
# TODO
# ----
# Make this structured code. It's a mess.
# Repeat what we just played for EXT files too
# Support HTTP Auth on ogg streams
# License, cleanup and package
#
# Jered Floyd <jered@mit.edu> takes very little credit for this code
# apparently neither does Quentin Smith <quentin@mit.edu>

use strict;
use warnings;
use Image::ExifTool qw(ImageInfo);
use File::Spec::Functions;
use File::Temp qw{tempfile tempdir};
use File::Basename qw(basename);
use LWP::UserAgent;
use Data::Dumper;
use IPC::Open2;
use English;

use vars qw/$zephyr_class $host $queue $mixer $channel/;

require "/usr/lib/gutenbach/config/gutenbach-filter-config.pl" or die "Unable to load configuration";

my $ua = new LWP::UserAgent;

# This variable contains the pid of the child process (which runs
# mplayer) once it has been forked, so that we can kill it on SIGTERM
my $pid;

# Replace STDERR with a log file in /tmp.
open(CUPS, ">&STDERR") or die "Unable to copy CUPS filehandle";
close(STDERR);
open(STDERR, ">>", "/tmp/gutenbach.log") or warn "Couldn't open log: $!";

# Set the TERM environment (for the benefit of mplayer?)
# I don't know why we do this --quentin
$ENV{"TERM"}="vt100";

print STDERR "STDERR FROM SPOOL FILTER\n";

# CUPS provides us with these arguments:
#
# argv[1]
# The job ID
# argv[2]
# The user printing the job
# argv[3]
# The job name/title
# argv[4]
# The number of copies to print
# argv[5]
# The options that were provided when the job was submitted
# argv[6]
# The file to print (first program only)
#
# The scheduler runs one or more of these programs to print any given
# job. The first filter reads from the print file and writes to the
# standard output, while the remaining filters read from the standard
# input and write to the standard output. The backend is the last
# filter in the chain and writes to the device.

printf(STDERR "Got \@ARGV: %s\n", Dumper(\@ARGV));

my %arguments = (
		 "job-id" => $ARGV[0],
		 user => $ARGV[1],
		 "job-title" => $ARGV[2],
		 copies => $ARGV[3],
		 options => {split(/[= ]/, $ARGV[4])},
		 file => $ARGV[5],
		);

# If we weren't given a filename, we need to read from stdin. Since
# mplayer really wants a file, let's write out to a temporary file
# first.
if (!$arguments{"file"}) {
  my ($fh, $file) = tempfile("gutenbachXXXXX", UNLINK => 1); # Ask File::Temp for a safe temporary file
  my $buf;
  while (read(STDIN, $buf, 1024*1024)) { # Read 1M at a time and put it in the temporary file
    print $fh $buf;
  }
  close($fh);
  $arguments{"file"} = $file;
}

printf(STDERR "Got \%arguments: %s\n", Dumper(\%arguments));

# Open up a zwrite command to announce the current track.
my @zwrite_command = (qw(/usr/bin/zwrite -d -n -c), $zephyr_class, "-i", $queue.'@'.$host, "-s", "Gutenbach Music Spooler");

print STDERR "Invoking @zwrite_command\n";
open(ZEPHYR, "|-", @zwrite_command) or die "Couldn't launch zwrite: $!";

my $status;
if (exists($arguments{"options"}{"job-originating-host-name"})) {
    print(ZEPHYR $arguments{"user"},"\@",$arguments{"options"}{"job-originating-host-name"}," is playing:\n");
    $status = "User: ".$arguments{"user"}."\@".$arguments{"options"}{"job-originating-host-name"};
} else {
    print(ZEPHYR $arguments{"user"}," is playing:\n");
    $status = "User: ".$arguments{"user"};
}

# SIGHUP handler, in case we were aborted
sub clear_status {
  kill 15, $pid if $pid;
  my @zwrite_command = (qw(/usr/bin/zwrite -d -n -c), $zephyr_class, "-i", $queue.'@'.$host, "-s", "Gutenbach Music Spooler");
  open(ZEPH, "|-", @zwrite_command);
  print(ZEPH "Playback aborted.\n");
  close(ZEPH);
  die;
}

$SIG{HUP} = \&clear_status;
$SIG{TERM} = \&clear_status;
$SIG{INT} = \&clear_status;

# Read the metadata information from the file.
my ($filepath) = $arguments{"file"};
my ($fileinfo) = ImageInfo($filepath);
my ($magic) = $fileinfo->{FileType};
my ($tempdir);
my ($newpath);

if ($magic) {
  # $magic means that Image::ExifTool was able to identify the type of file
  printf(ZEPHYR "%s file %s\n", $magic, $arguments{"job-title"});
  $status .= sprintf(" Filetype: %s.", $magic);
  $status .= sprintf(" Filename: %s.", $arguments{"job-title"});

  if (exists $fileinfo->{'Title'}) {
    printf(ZEPHYR "\@b{%s}\n", $fileinfo->{'Title'}) if exists $fileinfo->{'Title'};
    $status .= sprintf(" Title: %s.", $fileinfo->{'Title'});
  }
  foreach my $key (qw/Artist Album AlbumArtist/) {
    if (exists $fileinfo->{$key}) {
      printf(ZEPHYR "%s\n", $fileinfo->{$key}) if exists $fileinfo->{$key};
      $status .= sprintf(" %s: %s\n", $key, $fileinfo->{$key});
    }
  }

  $tempdir = tempdir();
  #awful hack -- geofft
  #== -- quentin
  # This code appears to create a new temporary directory and symlink
  # the job file into the temporary directory under the original
  # filename. I think this is because mplayer sometimes uses the file
  # extension to identify a filetype.
  $newpath = $tempdir . '/' . basename($arguments{"job-title"});
  symlink($filepath, $newpath);
  $filepath = $newpath;
}
elsif ($arguments{copies} == 42) {
  # This is a flag that is set by jobs queued by split_playlist(); it tells us to not try to split the playlist again.
  # Call resolve_external_reference to apply some heuristics to determine the filetype.
  $filepath = resolve_external_reference($filepath, \%arguments);
  if ($filepath =~ m|http://www\.youtube\.com/watch\?v=|) {
    # YouTube URLs are resolved by the youtube-dl command.
    # Launch youtube-dl
    $pid = open(YTDL, "-|");
    if ($pid) {
	print ZEPHYR "YouTube video $filepath.\nCurrently downloading, please wait...";
	close ZEPHYR;
	while (<YTDL>) {
	    open(ZEPHYR, "|-", @zwrite_command);
	    print ZEPHYR $_;
	    close ZEPHYR;
	}
	open(ZEPHYR, "|-", @zwrite_command);
	print ZEPHYR "Done downloading.";
	close ZEPHYR;
    }
    else {
	my @args = ("youtube-dl", "-q", "-o", "/tmp/youtube.flv", $filepath);
	exec(@args) or die "Couldn't exec youtube-dl";
    }

    $filepath = "/tmp/youtube.flv";
    # Print the title to zephyr and the status string.
  } else { # Doesn't appear to be a YouTube URL.
    print STDERR "Resolved external reference to $filepath\n";
    printf(ZEPHYR "%s\n", $filepath);
    $status .= sprintf(" External: %s\n", $filepath);
  }
}
elsif (-T $filepath) { # If the file appears to be a text file, treat it as a playlist.
  split_playlist($filepath, \%arguments);
  close(ZEPHYR);
  # See http://www.cups.org/documentation.php/api-filter.html#MESSAGES
  print CUPS "NOTICE: $status\n";
  exit 0;
}

close(ZEPHYR);
print CUPS "NOTICE: $status\n";
play_mplayer_audio($filepath, \%arguments);

# Remove the symlink we made earlier for the filetype.
if ($magic) {
  unlink($newpath);
  rmdir($tempdir);
}

# Play an external stream reference
sub resolve_external_reference {
  # Retrieve those command line opts.
  my ($filepath, $arguments) = @_;

  my ($format, $uri, $userpass);

  open(FILE, "<", $filepath) or die "Couldn't open spool file";
  if (<FILE> =~ /^(\S+)/) {
    # Take the leading non-whitespace as a URL
    $uri=$1;

    if ($uri =~ m|http://www\.youtube\.com/watch\?v=|) {
      return $uri;
    }

    # Fetch the URL with a HEAD request to get the content type
    my $response = $ua->head($uri);

    my $contenttype=($response->content_type() or "unknown");

    if ($contenttype eq "audio/mpeg") { $format="MP3" }
    elsif ($contenttype eq "application/x-ogg") { $format="OGG" }
    elsif ($contenttype eq "application/ogg") { $format="OGG" }
    elsif ($contenttype eq "audio/x-scpls") { $format="SHOUTCAST" }
    else {
      print ZEPHYR
	"Unknown Content-Type $contenttype for URI $uri\n";
    }
  } else { # Unable to match the URL regex
    print ZEPHYR "Couldn't read URI for external reference\n";
    # Return the existing path, in the hopes that mplayer knows what to do with it.
    return $filepath;
  }

  if ($format eq "SHOUTCAST") {
    print ZEPHYR "Shoutcast playlist...\n";
    return get_shoutcast($uri);
  } elsif ($format eq "MP3") {
  } elsif ($format eq "OGG") {
  } else {
    print ZEPHYR "Unrecognized stream format: $format\n";
  }
  return $uri;
}

sub split_playlist {
  my ($file, $arguments) = @_;

  my $i = 0;

  open(FILE, "<", $filepath) or die "Couldn't open spool file";
  while (<FILE>) {
    chomp;
    if (/^([^#]\S+)/) {
      printf (STDERR "Found playlist line: %s\n", $_);
      $ENV{CUPS_SERVER}='localhost';
      open(LP, "|-", "lp", "-d", "$queue", "-n", "42"); #'-#', '42', '-J', $arguments->{"job-title"}, '-o', 'job-priority=100');
      print LP $1;
      close(LP);
      $i++;
    }
  }
  printf(ZEPHYR "Playlist containing %d valid entries, split into separate jobs.\n", $i);
}

# Process a Shoutcast playlist
# get_shoutcast(URI)
sub get_shoutcast {
  my $uri = shift(@_);

  my $response = $ua->get($uri);
  my (@titles, @uris);

  foreach (split("\n", $response->content())) {
      if (/^File\d+=(\S+)/) {
	  push(@uris, $1);
      }
      if (/^Title\d+=(.+)$/) {
	  push(@titles, $1);
      }
  }

  # choose a random server
  my $server = int(rand scalar(@uris));
  # print the name of the stream if available
  print ZEPHYR "$titles[$server]\n";
  return $uris[$server];
}

sub play_mplayer_audio {
  my ($filepath, $opts) = @_;

  # Open up a zwrite command to show the mplayer output
  my @zwrite_command = (qw(/usr/bin/zwrite -d -n -c), $zephyr_class, "-i", $queue.'@'.$host, "-s", "Gutenbach Music Spooler");

  print STDERR "Invoking (from play_mplayer_audio): @zwrite_command\n";

  # fork for mplayer
  $pid = open(MP3STATUS, "-|");
  unless (defined $pid) {
    open(ZEPHYR, "|-", @zwrite_command) or die "Couldn't launch zwrite: $!";
    print ZEPHYR "Couldn't fork: $!\n";
    close(ZEPHYR);
    return;
  }

  if ($pid) { #parent
    # Check if there were any errors
    if ($_ = <MP3STATUS>) {
      open(ZEPHYR, "|-", @zwrite_command) or die "Couldn't launch zwrite: $!";
      print ZEPHYR "Playback completed with the following errors:\n";
      while (<MP3STATUS>) {
	print ZEPHYR $_;
      }
      close(ZEPHYR);
    } else {
      open(ZEPHYR, "|-", @zwrite_command) or die "Couldn't launch zwrite: $!";
      print ZEPHYR "Playback completed successfully.\n";
      close(ZEPHYR);
    }
    close(MP3STATUS) || print ZEPHYR "mplayer exited $?\n";
  }
  else { # child
    # redirect STDERR to STDOUT
    open STDERR, '>&STDOUT';
    # make sure that mplayer doesn't try to intepret the file as keyboard input
    close(STDIN);
    open(STDIN, "/dev/null");

    my @args = (qw|/usr/bin/mplayer -vo fbdev2 -zoom -x 1024 -y 768 -framedrop -nolirc -cache 512 -ao alsa -really-quiet |, $filepath);
    #pint STDERR "About to exec: ", Dumper([@args]);
    exec(@args) ||
      die "Couldn't exec";
  }
}
