source: debian/lib/gutenbach @ 5bb5b34

debianmacno-cupsnodebathenaweb
Last change on this file since 5bb5b34 was 5bb5b34, checked in by Jessica B. Hamrick <jhamrick@…>, 14 years ago

Have the CUPS daemon run under group audio

  • Property mode set to 100755
File size: 10.1 KB
RevLine 
[d988d9d]1#!/usr/bin/perl
[b0d9dda]2# Play the data on STDIN as an audio file
3#
[2e8b589]4# $Id: gutenbach-filter,v 1.26 2009/02/20 00:27:17 geofft Exp root $
5# $Source: /usr/local/bin/RCS/gutenbach-filter,v $
[b0d9dda]6#
7# TODO
8# ----
9# Make this structured code. It's a mess.
10# Repeat what we just played for EXT files too
11# Support HTTP Auth on ogg streams
12# License, cleanup and package
13#
14# Jered Floyd <jered@mit.edu> takes very little credit for this code
[c711ed9]15# apparently neither does Quentin Smith <quentin@mit.edu>
[b0d9dda]16
[d2fa590]17use strict;
18use warnings;
[ba06c7f]19use Image::ExifTool qw(ImageInfo);
20use File::Spec::Functions;
[d2fa590]21use File::Temp qw{tempfile tempdir};
[908107d]22use File::Basename qw(basename);
[ba06c7f]23use LWP::UserAgent;
24use Data::Dumper;
[f6c9736]25use IPC::Open2;
[d2fa590]26use English;
[ba06c7f]27
[e20354c]28use vars qw/$zephyr_class $host $queue $mixer $channel/;
29
[d2fa590]30require "/usr/lib/gutenbach/config/gutenbach-filter-config.pl" or die "Unable to load configuration";
[3a900ef]31
[ba06c7f]32my $ua = new LWP::UserAgent;
33
[d2fa590]34# Replace STDERR with a log file in /tmp.
35open(CUPS, ">&STDERR") or die "Unable to copy CUPS filehandle";
[ba06c7f]36close(STDERR);
[2e8b589]37open(STDERR, ">>", "/tmp/gutenbach.log") or warn "Couldn't open log: $!";
[ba06c7f]38
[d2fa590]39# Set the TERM environment (for the benefit of mplayer?)
40# I don't know why we do this --quentin
[ba06c7f]41$ENV{"TERM"}="vt100";
[b0d9dda]42
[148111e]43print STDERR "STDERR FROM SPOOL FILTER\n";
44
[d2fa590]45# CUPS provides us with these arguments:
46#
47# argv[1]
48# The job ID
49# argv[2]
50# The user printing the job
51# argv[3]
52# The job name/title
53# argv[4]
54# The number of copies to print
55# argv[5]
56# The options that were provided when the job was submitted
57# argv[6]
58# The file to print (first program only)
59#
60# The scheduler runs one or more of these programs to print any given
61# job. The first filter reads from the print file and writes to the
62# standard output, while the remaining filters read from the standard
63# input and write to the standard output. The backend is the last
64# filter in the chain and writes to the device.
[b0d9dda]65
[06133dc]66printf(STDERR "Got \@ARGV: %s\n", Dumper(\@ARGV));
67
[d2fa590]68my %arguments = (
[e20354c]69                 "job-id" => $ARGV[0],
[d2fa590]70                 user => $ARGV[1],
[e20354c]71                 "job-title" => $ARGV[2],
[d2fa590]72                 copies => $ARGV[3],
73                 options => {split(/[= ]/, $ARGV[4])},
74                 file => $ARGV[5],
75                );
76
77# If we weren't given a filename, we need to read from stdin. Since
78# mplayer really wants a file, let's write out to a temporary file
79# first.
80if (!$arguments{"file"}) {
81  my ($fh, $file) = tempfile("gutenbachXXXXX", UNLINK => 1); # Ask File::Temp for a safe temporary file
82  my $buf;
83  while (read(STDIN, $buf, 1024*1024)) { # Read 1M at a time and put it in the temporary file
84    print $fh $buf;
[06133dc]85  }
[d2fa590]86  close($fh);
[e20354c]87  $arguments{"file"} = $file;
[06133dc]88}
89
[d2fa590]90printf(STDERR "Got \%arguments: %s\n", Dumper(\%arguments));
[b011fb0]91
[d2fa590]92# Open up a zwrite command to announce the current track.
93my @zwrite_command = (qw(/usr/bin/zwrite -d -n -c), $zephyr_class, "-i", $queue.'@'.$host, "-s", "Gutenbach Music Spooler");
[80bbeea]94
[d2fa590]95print STDERR "Invoking @zwrite_command\n";
96open(ZEPHYR, "|-", @zwrite_command) or die "Couldn't launch zwrite: $!";
[b0d9dda]97
[3778176]98my $status;
99if (exists($arguments{"options"}{"job-originating-host-name"})) {
100    print(ZEPHYR $arguments{"user"},"\@",$arguments{"options"}{"job-originating-host-name"}," is playing:\n");
101    $status = "User: ".$arguments{"user"}."\@".$arguments{"options"}{"job-originating-host-name"};
102} else {
103    print(ZEPHYR $arguments{"user"}," is playing:\n");
104    $status = "User: ".$arguments{"user"};
105}
[2e8b589]106
[d2fa590]107# SIGHUP handler, in case we were aborted
[f014bc4]108sub clear_status {
[d2fa590]109  my @zwrite_command = (qw(/usr/bin/zwrite -d -n -c), $zephyr_class, "-i", $queue.'@'.$host, "-s", "Gutenbach Music Spooler");
110  open(ZEPH, "|-", @zwrite_command);
111  print(ZEPH "Playback aborted.\n");
112  close(ZEPH);
113  die;
[f014bc4]114}
115$SIG{HUP} = \&clear_status;
116
[ba06c7f]117# Read the metadata information from the file.
[e20354c]118my ($filepath) = $arguments{"file"};
[ba06c7f]119my ($fileinfo) = ImageInfo($filepath);
120my ($magic) = $fileinfo->{FileType};
[e20354c]121my ($tempdir);
122my ($newpath);
[b0d9dda]123
[ba06c7f]124if ($magic) {
[d2fa590]125  # $magic means that Image::ExifTool was able to identify the type of file
126  printf(ZEPHYR "%s file %s\n", $magic, $arguments{"job-title"});
127  $status .= sprintf(" Filetype: %s.", $magic);
128  $status .= sprintf(" Filename: %s.", $arguments{"job-title"});
129
130  if (exists $fileinfo->{'Title'}) {
131    printf(ZEPHYR "\@b{%s}\n", $fileinfo->{'Title'}) if exists $fileinfo->{'Title'};
132    $status .= sprintf(" Title: %s.", $fileinfo->{'Title'});
133  }
134  foreach my $key (qw/Artist Album AlbumArtist/) {
135    if (exists $fileinfo->{$key}) {
136      printf(ZEPHYR "%s\n", $fileinfo->{$key}) if exists $fileinfo->{$key};
137      $status .= sprintf(" %s: %s\n", $key, $fileinfo->{$key});
[ba06c7f]138    }
[d2fa590]139  }
140
[e20354c]141  $tempdir = tempdir();
[d2fa590]142  #awful hack -- geofft
143  #== -- quentin
144  # This code appears to create a new temporary directory and symlink
145  # the job file into the temporary directory under the original
146  # filename. I think this is because mplayer sometimes uses the file
147  # extension to identify a filetype.
[e20354c]148  $newpath = $tempdir . '/' . basename($arguments{"job-title"});
[d2fa590]149  symlink($filepath, $newpath);
150  $filepath = $newpath;
[ba06c7f]151}
[d2fa590]152elsif ($arguments{copies} == 42) {
153  # This is a flag that is set by jobs queued by split_playlist(); it tells us to not try to split the playlist again.
154  # Call resolve_external_reference to apply some heuristics to determine the filetype.
155  $filepath = resolve_external_reference($filepath, \%arguments);
156  if ($filepath =~ m|http://www\.youtube\.com/watch\?v=|) {
157    # YouTube URLs are resolved by the youtube-dl command.
158    # Launch youtube-dl
159    open(YTDL, "-|", "youtube-dl", "-g", $filepath) or die "Unable to invoke youtube-dl";
160    # Read the title (currently not doing so because youtube-dl doesn't know how to get the title.
161    my $title = ""; # <YTDL>
162    # Print the title to zephyr and the status string.
163    print ZEPHYR "YouTube video $filepath\n$title";
164    $status .= " YouTube video $filepath. $title.";
165    # youtube-dl prints the URL of the flash video, which we pass to mplayer as a filename.
166    $filepath = <YTDL>;
167    chomp $filepath;
168  } else { # Doesn't appear to be a YouTube URL.
169    print STDERR "Resolved external reference to $filepath\n";
170    printf(ZEPHYR "%s\n", $filepath);
171    $status .= sprintf(" External: %s\n", $filepath);
172  }
[ba06c7f]173}
[d2fa590]174elsif (-T $filepath) { # If the file appears to be a text file, treat it as a playlist.
175  split_playlist($filepath, \%arguments);
176  close(ZEPHYR);
177  # See http://www.cups.org/documentation.php/api-filter.html#MESSAGES
178  print CUPS "NOTICE: $status\n";
179  exit 0;
[7e27cc3]180}
[a041c73]181
[ba06c7f]182close(ZEPHYR);
[d2fa590]183print CUPS "NOTICE: $status\n";
184play_mplayer_audio($filepath, \%arguments);
[b0d9dda]185
[d2fa590]186# Remove the symlink we made earlier for the filetype.
[a041c73]187if ($magic) {
[d2fa590]188  unlink($newpath);
189  rmdir($tempdir);
[a041c73]190}
191
[b0d9dda]192# Play an external stream reference
[ba06c7f]193sub resolve_external_reference {
[d2fa590]194  # Retrieve those command line opts.
195  my ($filepath, $arguments) = @_;
196
[e20354c]197  my ($format, $uri, $userpass);
[d2fa590]198
199  open(FILE, "<", $filepath) or die "Couldn't open spool file";
200  if (<FILE> =~ /^(\S+)/) {
201    # Take the leading non-whitespace as a URL
202    $uri=$1;
203
204    if ($uri =~ m|http://www\.youtube\.com/watch\?v=|) {
205      return $uri;
[b281379]206    }
[b0d9dda]207
[d2fa590]208    # Fetch the URL with a HEAD request to get the content type
209    my $response = $ua->head($uri);
210
211    my $contenttype=($response->content_type() or "unknown");
212
213    if ($contenttype eq "audio/mpeg") { $format="MP3" }
214    elsif ($contenttype eq "application/x-ogg") { $format="OGG" }
215    elsif ($contenttype eq "application/ogg") { $format="OGG" }
216    elsif ($contenttype eq "audio/x-scpls") { $format="SHOUTCAST" }
217    else {
218      print ZEPHYR
219        "Unknown Content-Type $contenttype for URI $uri\n";
[b0d9dda]220    }
[d2fa590]221  } else { # Unable to match the URL regex
222    print ZEPHYR "Couldn't read URI for external reference\n";
223    # Return the existing path, in the hopes that mplayer knows what to do with it.
224    return $filepath;
225  }
226
227  if ($format eq "SHOUTCAST") {
228    print ZEPHYR "Shoutcast playlist...\n";
229    return get_shoutcast($uri);
230  } elsif ($format eq "MP3") {
231  } elsif ($format eq "OGG") {
232  } else {
233    print ZEPHYR "Unrecognized stream format: $format\n";
234  }
235  return $uri;
[b0d9dda]236}
237
[7e27cc3]238sub split_playlist {
[d2fa590]239  my ($file, $arguments) = @_;
240
241  my $i = 0;
242
243  open(FILE, "<", $filepath) or die "Couldn't open spool file";
244  while (<FILE>) {
245    chomp;
246    if (/^([^#]\S+)/) {
247      printf (STDERR "Found playlist line: %s\n", $_);
248      open(LPR, "|-", 'lpr', '-P'.$queue.'@localhost', '-#', '42', '-J', $arguments->{"job-title"}, '-o', 'job-priority=100');
249      print LPR $1;
250      close(LPR);
251      $i++;
[7e27cc3]252    }
[d2fa590]253  }
254  printf(ZEPHYR "Playlist containing %d valid entries, split into separate jobs.\n", $i);
[7e27cc3]255}
256
[53d7625]257# Process a Shoutcast playlist
258# get_shoutcast(URI)
259sub get_shoutcast {
260  my $uri = shift(@_);
[d2fa590]261
[ba06c7f]262  my $response = $ua->get($uri);
[e20354c]263  my (@titles, @uris);
[ba06c7f]264
265  foreach (split("\n", $response->content())) {
[53d7625]266      if (/^File\d+=(\S+)/) {
267          push(@uris, $1);
268      }
269      if (/^Title\d+=(.+)$/) {
270          push(@titles, $1);
271      }
272  }
[d2fa590]273
[53d7625]274  # choose a random server
[e20354c]275  my $server = int(rand scalar(@uris));
[53d7625]276  # print the name of the stream if available
277  print ZEPHYR "$titles[$server]\n";
278  return $uris[$server];
279}
[b0d9dda]280
[ba06c7f]281sub play_mplayer_audio {
[d2fa590]282  my ($filepath, $opts) = @_;
[b0d9dda]283
284
[d2fa590]285  # Open up a zwrite command to show the mplayer output
286  my @zwrite_command = (qw(/usr/bin/zwrite -d -n -c), $zephyr_class, "-i", $queue.'@'.$host, "-s", "Gutenbach Music Spooler");
287
288  print STDERR "Invoking @zwrite_command\n";
289  open(ZEPHYR, "|-", @zwrite_command) or die "Couldn't launch zwrite: $!";
290
291  # fork for mplayer
292  my $pid = open(MP3STATUS, "-|");
293  unless (defined $pid) {
294    print ZEPHYR "Couldn't fork: $!\n";
295    close(ZEPHYR);
296    return;
[b0d9dda]297  }
298
[d2fa590]299  if ($pid) { #parent
300    # Check if there were any errors
301    if ($_ = <MP3STATUS>) {
302      print ZEPHYR "Playback completed with the following errors:\n";
303      print ZEPHYR $_;
304      while (<MP3STATUS>) {
305        print ZEPHYR $_;
306      }
307    } else {
308      print ZEPHYR "Playback completed successfully.\n";
309    }
310    close(MP3STATUS) || print ZEPHYR "mplayer exited $?\n";
[b0d9dda]311
[d2fa590]312    close(ZEPHYR);
313  }
314  else { # child
315    # redirect STDERR to STDOUT
316    open STDERR, '>&STDOUT';
317    # make sure that mplayer doesn't try to intepret the file as keyboard input
318    close(STDIN);
319    open(STDIN, "/dev/null");
320
[5bb5b34]321    my @args = (qw|/usr/bin/mplayer -vo fbdev2 -zoom -x 1024 -y 768 -framedrop -nolirc -ao alsa -cache 512 -really-quiet |, $filepath);
[d2fa590]322    #print STDERR "About to exec: ", Dumper([@args]);
323    exec(@args) ||
324      die "Couldn't exec";
325  }
326}
Note: See TracBrowser for help on using the repository browser.