#!/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 takes very little credit for this code # apparently neither does Quentin Smith 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; # 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 open(YTDL, "-|", "youtube-dl", "-g", $filepath) or die "Unable to invoke youtube-dl"; # Read the title (currently not doing so because youtube-dl doesn't know how to get the title. my $title = ""; # # Print the title to zephyr and the status string. print ZEPHYR "YouTube video $filepath\n$title"; $status .= " YouTube video $filepath. $title."; # youtube-dl prints the URL of the flash video, which we pass to mplayer as a filename. $filepath = ; chomp $filepath; } 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 ( =~ /^(\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 () { chomp; if (/^([^#]\S+)/) { printf (STDERR "Found playlist line: %s\n", $_); open(LPR, "|-", 'lpr', '-P'.$queue.'@localhost', '-#', '42', '-J', $arguments->{"job-title"}, '-o', 'job-priority=100'); print LPR $1; close(LPR); $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 ($_ = ) { open(ZEPHYR, "|-", @zwrite_command) or die "Couldn't launch zwrite: $!"; print ZEPHYR "Playback completed with the following errors:\n"; while () { 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); #print STDERR "About to exec: ", Dumper([@args]); exec(@args) || die "Couldn't exec"; } }