source: server/lib/gutenbach @ 4e5ee08

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

Fix the gutenbach backend, such that it updates
/var/run/gutenbach/status with the currently playing song.
gutenbach-remctl needs this file to exist so that it can respond to
the status get command (and, thus, gutenbach-web needs it, since it
uses the command to display the current status).

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