Index: rver/lib/gutenbach
===================================================================
--- server/lib/gutenbach	(revision a2944f6cb79c7542cc207952b742f3b628290dfa)
+++ 	(revision )
@@ -1,348 +1,0 @@
-#!/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);
-
-  open(STATUS, ">", "/var/run/gutenbach/status");
-  print(STATUS "");
-  close(STATUS);
-  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);
-my ($title);
-
-open(STATUS, ">", "/var/run/gutenbach/status");
-
-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"});
-  #printf(STATUS $arguments{"job-title"});
-  $status .= sprintf(" Filetype: %s.", $magic);
-  $status .= sprintf(" Filename: %s.", $arguments{"job-title"});
-  if (exists $fileinfo->{'Title'}) {
-      $title = $fileinfo->{'Title'};
-    printf(ZEPHYR "\@b{%s}\n", $fileinfo->{'Title'}) if exists $fileinfo->{'Title'};
-    printf(STATUS "%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};
-      printf(STATUS "%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, "-|", "youtube-dl","-b", "-g", $filepath) or die "Unable to invoke youtube-dl";
-	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 = <YTDL>;
-	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);
-close(STATUS);
-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);
-      open(STATUS, ">", "/var/run/gutenbach/status");
-      print(STATUS "");
-      close(STATUS);
-    }
-    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";
-  }
-}
Index: server/lib/gutenbach-filter
===================================================================
--- server/lib/gutenbach-filter	(revision df510617607a17049c40187f93c7ce3d26508ae1)
+++ server/lib/gutenbach-filter	(revision df510617607a17049c40187f93c7ce3d26508ae1)
@@ -0,0 +1,348 @@
+#!/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);
+
+  open(STATUS, ">", "/var/run/gutenbach/status");
+  print(STATUS "");
+  close(STATUS);
+  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);
+my ($title);
+
+open(STATUS, ">", "/var/run/gutenbach/status");
+
+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"});
+  #printf(STATUS $arguments{"job-title"});
+  $status .= sprintf(" Filetype: %s.", $magic);
+  $status .= sprintf(" Filename: %s.", $arguments{"job-title"});
+  if (exists $fileinfo->{'Title'}) {
+      $title = $fileinfo->{'Title'};
+    printf(ZEPHYR "\@b{%s}\n", $fileinfo->{'Title'}) if exists $fileinfo->{'Title'};
+    printf(STATUS "%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};
+      printf(STATUS "%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, "-|", "youtube-dl","-b", "-g", $filepath) or die "Unable to invoke youtube-dl";
+	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 = <YTDL>;
+	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);
+close(STATUS);
+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);
+      open(STATUS, ">", "/var/run/gutenbach/status");
+      print(STATUS "");
+      close(STATUS);
+    }
+    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";
+  }
+}
Index: server/lib/gutenbach/ipp/__init__.py
===================================================================
--- server/lib/gutenbach/ipp/__init__.py	(revision df510617607a17049c40187f93c7ce3d26508ae1)
+++ server/lib/gutenbach/ipp/__init__.py	(revision df510617607a17049c40187f93c7ce3d26508ae1)
@@ -0,0 +1,5 @@
+from attribute import Attribute
+from attributegroup import AttributeGroup
+from request import Request
+from value import Value
+from constants import Tags
Index: server/lib/gutenbach/ipp/attribute.py
===================================================================
--- server/lib/gutenbach/ipp/attribute.py	(revision df510617607a17049c40187f93c7ce3d26508ae1)
+++ server/lib/gutenbach/ipp/attribute.py	(revision df510617607a17049c40187f93c7ce3d26508ae1)
@@ -0,0 +1,155 @@
+#!/usr/bin/python
+
+import sys, struct, logging
+from .value import Value
+
+# initialize logger
+logger = logging.getLogger(__name__)
+
+class Attribute(object):
+    """
+    In addition to what the RFC reports, an attribute has an
+    'attribute tag', which specifies what type of attribute it is.
+    It is 1 bytes long, and comes before the list of values.
+
+    From RFC 2565:
+
+    Each attribute consists of:
+    -----------------------------------------------
+    |                   value-tag                 |   1 byte
+    -----------------------------------------------
+    |               name-length  (value is u)     |   2 bytes
+    -----------------------------------------------
+    |                     name                    |   u bytes
+    -----------------------------------------------
+    |              value-length  (value is v)     |   2 bytes
+    -----------------------------------------------
+    |                     value                   |   v bytes
+    -----------------------------------------------
+
+    An additional value consists of:
+    -----------------------------------------------------------
+    |                   value-tag                 |   1 byte  |
+    -----------------------------------------------           |
+    |            name-length  (value is 0x0000)   |   2 bytes |
+    -----------------------------------------------           |-0 or more
+    |              value-length (value is w)      |   2 bytes |
+    -----------------------------------------------           |
+    |                     value                   |   w bytes |
+    -----------------------------------------------------------
+    """
+
+    def __init__(self, name=None, values=None):
+        """
+        Initialize an Attribute.  This function can be called in three
+        different ways:
+
+            Attribute() -- creates an empty Attribute
+
+            Attribute(name) -- creates an empty Attribute with a name
+
+            Attribute(name, values) -- creates an Attribute
+            initialized with a name and list of values
+        
+        Arguments:
+
+            name -- the name of the attribute
+
+            values -- a list of Values.  May not be empty.
+        """
+
+        if name is not None:
+            assert isinstance(name, str), \
+                   "Attribute name must be a string!"
+        if values is None:
+            values = []
+        for value in values:
+            assert isinstance(value, Value), \
+                   "Value %r must be of type Value" % (value)
+
+        self.name = name
+        self.values = values
+
+    @property
+    def packed_value(self):
+        """
+        Packs the attribute data into binary data.
+        """
+
+        assert self.name is not None, \
+               "cannot pack unnamed attribute!"
+        assert len(self.values) > 0, \
+               "cannot pack empty attribute!"
+
+        # get the binary data for all the values
+        values = []
+        for v, i in zip(self.values, xrange(len(self.values))):
+
+            # get the name length (0 for everything but the first
+            # value)
+            if i == 0:
+                name_length = len(self.name)
+            else:
+                name_length = 0
+
+            # get the value length and binary value
+            value_bin = v.packed_value
+            value_length = len(value_bin)
+
+            logger.debug("dumping name_length : %i" % name_length)
+            logger.debug("dumping name : %s" % self.name)
+            logger.debug("dumping value_length : %i" % value_length)
+            logger.debug("dumping value : %s" % v.value)
+
+            # the value tag in binary
+            value_tag_bin = struct.pack('>b', v.value_tag)
+
+            # the name length in binary
+            name_length_bin = struct.pack('>h', name_length)
+
+            # the name in binary
+            name_bin = self.name
+
+            # the value length in binary
+            value_length_bin = struct.pack('>h', value_length)
+
+            if i == 0:
+                values.append(''.join([value_tag_bin,
+                                       name_length_bin,
+                                       name_bin,
+                                       value_length_bin,
+                                       value_bin]))
+            else:
+                values.append(''.join([value_tag_bin,
+                                       name_length_bin,
+                                       value_length_bin,
+                                       value_bin]))
+
+        # concatenate everything together and return it along with the
+        # total length of the attribute
+        return ''.join(values)
+
+    @property
+    def packed_value_size(self):
+        """
+        Gets the total size of the attribute.
+        """
+        return len(self.packed_value)
+
+    total_size = packed_value_size
+
+    def __str__(self):
+        if len(self.values) > 0:
+            values = [str(v) for v in self.values]
+        else:
+            values = "None"
+
+        if self.name is None:
+            name = "None"
+        else:
+            name = self.name
+        
+        return "%s: %s" % (name, str(values))
+
+    def __repr__(self):
+        return '<IPPAttribute (%r, %r)>' % (self.name, self.values)
Index: server/lib/gutenbach/ipp/attributegroup.py
===================================================================
--- server/lib/gutenbach/ipp/attributegroup.py	(revision df510617607a17049c40187f93c7ce3d26508ae1)
+++ server/lib/gutenbach/ipp/attributegroup.py	(revision df510617607a17049c40187f93c7ce3d26508ae1)
@@ -0,0 +1,109 @@
+#!/usr/bin/python
+
+import sys, struct, logging
+from .attribute import Attribute
+
+# initialize logger
+logger = logging.getLogger(__name__)
+
+class AttributeGroup(object):
+    """
+    An AttributeGroup consists of an attribute-group-tag, followed by
+    a sequence of Attributes. According to RFC 2565, "Within an
+    attribute-sequence, if two attributes have the same name, the
+    first occurrence MUST be ignored.", so we can effectively treat
+    this as an ordered dictionary.
+    """
+
+    def __init__(self, attribute_group_tag=None, attributes=[]):
+        """
+        Initialize an AttributeGroup.  An AttributeGroup can be
+        initialized in three ways:
+
+            AttributeGroup()
+            AttributeGroup(attribute_group_tag)
+            AttributeGroup(attribute_group_tag, attributes)
+
+        Arguments:
+
+            attribute_group_tag -- a signed char, holds the tag of the
+                                   attribute group
+
+            attributes -- a list of attributes
+        """
+
+        if attribute_group_tag is not None:
+            assert isinstance(attribute_group_tag, int), \
+                   "attribute_group_tag must be a character!"
+            
+
+        self.attribute_group_tag = attribute_group_tag
+        self.attributes = []
+        self.extend(attributes)
+
+    def __getitem__(self, name):
+        """
+        Returns a list of attributes which have name 'name'.
+        """
+        
+        attribute = filter(lambda x: x.name == name, self.attributes)
+        if attribute:
+            return attribute[0]
+        else:
+            raise KeyError("Attribute %r not found" % name)
+
+    def __contains__(self, name):
+        return len(filter(lambda x: x.name == name, self.attributes))
+
+    def __iter__(self):
+        return (a.name for a in self.attributes)
+
+    iterkeys = __iter__
+
+    def __setitem__(self, name, attribute):
+        """
+        Sets an attribute in the attribute group. Note that the key is
+        ignored and the attribute is queried for its name.
+        """
+
+        return self.append(attribute)
+
+    def __delitem__(self, name):
+        self.attributes = filter(lambda x: x.name != name,
+				 self.attributes)
+
+    def append(self, attribute):
+        return self.extend([attribute])
+
+    def extend(self, attributes):
+        """
+        Sets the attributes for the attribute group.
+        """
+
+        for a in attributes:
+            assert isinstance(a, Attribute), \
+                   "attribute must be of type Attribute!"
+
+        for a in attributes:
+            # XXX: Instead of replacing the attribute, do we want to
+            # append the value to the attribute here?
+            del self[a.name]
+            self.attributes.append(a)
+
+    @property
+    def packed_value(self):
+        """
+        Convert the AttributeGroup to binary.
+        """
+
+        # conver the attribute_group_tag to binary
+        tag = struct.pack('>b', self.attribute_group_tag)
+
+        # convert each of the attributes to binary
+        attributes = [a.packed_value for a in self.attributes]
+
+        # concatenate everything and return
+        return tag + ''.join(attributes)
+
+    def __repr__(self):
+        return '<IPPAttributeGroup (%r, %r)>' % (self.attribute_group_tag, self.attributes)
Index: server/lib/gutenbach/ipp/constants.py
===================================================================
--- server/lib/gutenbach/ipp/constants.py	(revision df510617607a17049c40187f93c7ce3d26508ae1)
+++ server/lib/gutenbach/ipp/constants.py	(revision df510617607a17049c40187f93c7ce3d26508ae1)
@@ -0,0 +1,314 @@
+#!/usr/bin/python
+
+class Misc():
+    """
+    Miscellaneous config options for the IPP server.
+    """
+    
+    IPP_VERSION = "1.1"
+    IPP_PORT = 8000
+
+    def __init__(self): pass
+
+class JobStates():
+    """
+    Job state codes, as defined by RFC 2911, Section 4.3.7
+    """
+    
+    PENDING    = 3
+    HELD       = 4
+    PROCESSING = 5
+    STOPPED    = 6
+    CANCELLED  = 7
+    ABORTED    = 8
+    COMPLETE   = 9
+
+    def __init__(self): pass
+
+class PrinterStates():
+    """
+    Printer state codes, as defined by RFC 2911, Section 4.4.11
+    """
+
+    IDLE       = 3
+    PROCESSING = 4
+    STOPPED    = 5
+
+    def __init__(self): pass
+
+class Operations():
+    """
+    IPP and CUPS IPP Operations, as defined in various RFCs:
+
+        0x0002 - 0x0012      RFC 2911 (Section 4.4.15)
+        0x0013 - 0x0015      RFC 3380 (Section 4)
+        0x0016 - 0x001b      RFC 3995 (Section 7.1)
+        0x0022 - 0x0031      RFC 3998 (Section 14.3)
+        0x4000 - 0x4027      CUPS IPP Actions
+    """
+
+    # These are defined in RFC 2911, Section 4.4.15
+    PRINT_JOB              = 0x0002
+    PRINT_URI              = 0x0003
+    VALIDATE_JOB           = 0x0004
+    CREATE_JOB             = 0x0005
+    SEND_DOCUMENT          = 0x0006
+    SEND_URI               = 0x0007
+    CANCEL_JOB             = 0x0008
+    GET_JOB_ATTRIBUTES     = 0x0009
+    GET_JOBS               = 0x000a
+    GET_PRINTER_ATTRIBUTES = 0x000b
+    #HOLD_JOB              = 0x000c
+    #RELEASE_JOB           = 0x000d
+    #RESTART_JOB           = 0x000e
+    PAUSE_PRINTER          = 0x0010
+    RESUME_PRINTER         = 0x0011
+    #PURGE_JOBS            = 0x0012
+
+    # These are defined in RFC 3380, Section 4
+    SET_PRINTER_ATTRIBUTES           = 0x0013
+    SET_JOB_ATTRIBUTES               = 0x0014
+    #GET_PRINTER_SUPPORTED_VALUES    = 0x0015
+
+    # These are defined in RFC 3995, Section 7.1
+    #CREATE_PRINTER_SUBSCRIPTION     = 0x0016
+    #CREATE_JOB_SUBSCRIPTION         = 0x0017
+    #GET_SUBSCRIPTION_ATTRIBUTES     = 0x0018
+    #GET_SUBSCRIPTIONS               = 0x0019
+    #RENEW_SUBSCRIPTION              = 0x001a
+    #CANCEL_SUBSCRIPTION             = 0x001b
+
+    # These are defined in RFC 3998, Section 14.3
+    #ENABLE_PRINTER                  = 0x0022
+    #DISABLE_PRINTER                 = 0x0023
+    #PAUSE_PRINTER_AFTER_CURRENT_JOB = 0x0024
+    #HOLD_NEW_JOBS                   = 0x0025
+    #RELEASE_HELD_NEW_JOBS           = 0x0026
+    #DEACTIVATE_PRINTER              = 0x0027
+    #ACTIVATE_PRINTER                = 0x0028
+    #RESTART_PRINTER                 = 0x0029
+    #SHUTDOWN_PRINTER                = 0x002a
+    #STARTUP_PRINTER                 = 0x002b
+    #REPROCESS_JOB                   = 0x002c
+    #CANCEL_CURRENT_JOB              = 0x002d
+    #SUSPEND_CURRENT_JOB             = 0x002e
+    #RESUME_JOB                      = 0x002f
+    PROMOTE_JOB                      = 0x0030
+    #SCHEDULE_JOB_AFTER              = 0x0031
+
+    # These are special CUPS actions, defined in:
+    # http://www.cups.org/documentation.php/spec-ipp.html
+    #PRIVATE               = 0x4000
+    CUPS_GET_DEFAULT       = 0x4001
+    CUPS_GET_PRINTERS      = 0x4002
+    #CUPS_ADD_PRINTER      = 0x4003
+    #CUPS_DELETE_PRINTER   = 0x4004
+    CUPS_GET_CLASSES       = 0x4005
+    #CUPS_ADD_CLASS        = 0x4006
+    #CUPS_DELETE_CLASS     = 0x4007
+    #CUPS_ACCEPT_JOBS      = 0x4008
+    #CUPS_REJECT_JOBS      = 0x4009
+    #CUPS_SET_DEFAULT      = 0x400a
+    #CUPS_GET_DEVICES      = 0x400b
+    #CUPS_GET_PPDS         = 0x400c
+    #CUPS_MOVE_JOB         = 0x400d
+    #CUPS_AUTHENTICATE_JOB = 0x400e
+    #CUPS_GET_PPD          = 0x400f
+    CUPS_GET_DOCUMENT      = 0x4027
+
+    def __init__(self): pass
+
+class SuccessCodes():
+    """
+    Success status codes as defined in RFC 2911, Section 13
+    """
+    
+    OK                           = 0x0000
+    OK_SUBST                     = 0x0001
+    OK_CONFLICT                  = 0x0002
+    OK_IGNORED_SUBSCRIPTIONS     = 0x0003
+    OK_IGNORED_NOTIFICATIONS     = 0x0004
+    OK_TOO_MANY_EVENTS           = 0x0005
+    OK_BUT_CANCEL_SUBSCRIPTION   = 0x0006
+
+    def __init__(self): pass
+
+class ClientErrorCodes():
+    """
+    Client error codes as defined in RFC 2911, Section 13
+    """
+    
+    BAD_REQUEST                  = 0x0400
+    FORBIDDEN                    = 0x0401
+    NOT_AUTHENTICATED            = 0x0402
+    NOT_AUTHORIZED               = 0x0403
+    NOT_POSSIBLE                 = 0x0404
+    TIMEOUT                      = 0x0405
+    NOT_FOUND                    = 0x0406
+    GONE                         = 0x0407
+    REQUEST_ENTITY               = 0x0408
+    REQUEST_VALUE                = 0x0409
+    DOCUMENT_FORMAT              = 0x040a
+    ATTRIBUTES                   = 0x040b
+    URI_SCHEME                   = 0x040c
+    CHARSET                      = 0x040d
+    CONFLICT                     = 0x040e
+    COMPRESSION_NOT_SUPPORTED    = 0x040f
+    COMPRESSION_ERROR            = 0x0410
+    DOCUMENT_FORMAT_ERROR        = 0x0411
+    DOCUMENT_ACCESS_ERROR        = 0x0412
+    ATTRIBUTES_NOT_SETTABLE      = 0x0413
+    IGNORED_ALL_SUBSCRIPTIONS    = 0x0414
+    TOO_MANY_SUBSCRIPTIONS       = 0x0415
+    IGNORED_ALL_NOTIFICATIONS    = 0x0416
+    PRINT_SUPPORT_FILE_NOT_FOUND = 0x0417
+
+    def __init__(self): pass
+
+class ServerErrorCodes():
+    """
+    Server error codes as defined in RFC 2911, Section 13
+    """
+
+    INTERNAL_ERROR              = 0x0500
+    OPERATION_NOT_SUPPORTED     = 0x0501
+    SERVICE_UNAVAILABLE         = 0x0502
+    VERSION_NOT_SUPPORTED       = 0x0503
+    DEVICE_ERROR                = 0x0504
+    TEMPORARY_ERROR             = 0x0505
+    NOT_ACCEPTING               = 0x0506
+    PRINTER_BUSY                = 0x0507
+    ERROR_JOB_CANCELLED         = 0x0508
+    MULTIPLE_JOBS_NOT_SUPPORTED = 0x0509
+    PRINTER_IS_DEACTIVATED      = 0x050a
+
+    def __init__(self): pass
+
+class StatusCodes(SuccessCodes, ClientErrorCodes, ServerErrorCodes):
+    pass
+
+class CUPSPrinterType():
+    """
+    Printer types as defined by cups_ptype_e in the CUPS API
+    specification:
+    
+    http://www.cups.org/documentation.php/doc-1.3/api-cups.html#cups_ptype_e
+    """
+
+    LOCAL         = 0x000000
+    CLASS         = 0x000001
+    REMOTE        = 0x000002
+    BW            = 0x000004
+    COLOR         = 0x000008
+
+    DUPLEX        = 0x000010
+    STAPLE        = 0x000020
+    COPIES        = 0x000040
+    COLLATE       = 0x000080
+
+    PUNCH         = 0x000100
+    COVER         = 0x000200
+    BIND          = 0x000400
+    SORT          = 0x000800
+
+    SMALL         = 0x001000
+    MEDIUM        = 0x002000
+    LARGE         = 0x004000
+    VARIABLE      = 0x008000
+
+    IMPLICIT      = 0x010000
+    DEFAULT       = 0x020000
+    FAX           = 0x040000
+    REJECTING     = 0x080000
+
+    DELETE        = 0x100000
+    NOT_SHARED    = 0x200000
+    AUTHENTICATED = 0x400000
+    COMMANDS      = 0x800000
+
+    OPTIONS       = 0x00e6ff
+
+    def __init__(self): pass
+
+class AttributeTags():
+    """
+    Contains constants for the attribute IPP tags, as defined by RFC
+    2565.
+    """
+    
+    ZERO_NAME_LENGTH   = 0x00
+    OPERATION          = 0x01
+    JOB                = 0x02
+    END                = 0x03
+    PRINTER            = 0x04
+    UNSUPPORTED        = 0x05
+    SUBSCRIPTION       = 0x06
+    EVENT_NOTIFICATION = 0x07
+
+    def __init__(self): pass
+
+class OutOfBandTags():
+    """
+    Contains constants for the out-of-band value IPP tags, as defined
+    by RFC 2565.
+    """
+    
+    UNSUPPORTED      = 0x10
+    DEFAULT          = 0x11
+    UNKNOWN          = 0x12
+    NO_VALUE         = 0x13
+    NOT_SETTABLE     = 0x15
+    DELETE_ATTRIBUTE = 0x16
+    ADMIN_DEFINE     = 0x17
+
+    def __init__(self): pass
+
+class IntegerTags():
+    """
+    Contains constants for the integer value IPP tags, as defined by
+    RFC 2565.
+    """
+    
+    GENERIC = 0x20
+    INTEGER = 0x21
+    BOOLEAN = 0x22
+    ENUM    = 0x23
+
+    def __init__(self): pass
+
+class OctetStringTags():
+    """
+    Contains constants for the octetString value IPP tags, as defined
+    by RFC 2565.
+    """
+    
+    UNSPECIFIED_OCTETSTRING = 0x30
+    DATETIME                = 0x31
+    RESOLUTION              = 0x32
+    RANGE_OF_INTEGER        = 0x33
+    BEG_COLLECTION          = 0x34
+    TEXT_WITH_LANGUAGE      = 0x35
+    NAME_WITH_LANGUAGE      = 0x36
+    END_COLLECTION          = 0x37
+
+    def __init__(self): pass
+
+class CharacterStringTags():
+    """
+    Contains constants for the character-string value IPP tags, as
+    defined by RFC 2565.
+    """
+    
+    GENERIC               = 0x40
+    TEXT_WITHOUT_LANGUAGE = 0x41
+    NAME_WITHOUT_LANGUAGE = 0x42
+    KEYWORD               = 0x44
+    URI                   = 0x45
+    URI_SCHEME            = 0x46
+    CHARSET               = 0x47
+    NATURAL_LANGUAGE      = 0x48
+    MIME_MEDIA_TYPE       = 0x49                                    
+    MEMBER_ATTR_NAME      = 0x4a
+
+class Tags(AttributeTags, OutOfBandTags, IntegerTags, OctetStringTags, CharacterStringTags):
+    pass
Index: server/lib/gutenbach/ipp/request.py
===================================================================
--- server/lib/gutenbach/ipp/request.py	(revision df510617607a17049c40187f93c7ce3d26508ae1)
+++ server/lib/gutenbach/ipp/request.py	(revision df510617607a17049c40187f93c7ce3d26508ae1)
@@ -0,0 +1,240 @@
+#!/usr/bin/python
+
+import sys, struct, logging
+from .attributegroup import AttributeGroup
+from .attribute import Attribute
+from .value import Value
+from .constants import AttributeTags
+
+# initialize logger
+logger = logging.getLogger(__name__)
+
+class Request():
+    """
+    From RFC 2565:
+    
+    The encoding for an operation request or response consists of:
+    -----------------------------------------------
+    |                  version-number             |   2 bytes  - required
+    -----------------------------------------------
+    |               operation-id (request)        |
+    |                      or                     |   2 bytes  - required
+    |               status-code (response)        |
+    -----------------------------------------------
+    |                   request-id                |   4 bytes  - required
+    -----------------------------------------------------------
+    |               xxx-attributes-tag            |   1 byte  |
+    -----------------------------------------------           |-0 or more
+    |             xxx-attribute-sequence          |   n bytes |
+    -----------------------------------------------------------
+    |              end-of-attributes-tag          |   1 byte   - required
+    -----------------------------------------------
+    |                     data                    |   q bytes  - optional
+    -----------------------------------------------
+    """
+
+    # either give the version, operation_id, request_id,
+    # attribute_sequence, and data, or a file handler (request) which
+    # can be read from to get the request
+    def __init__(self, version=None, operation_id=None, request_id=None,
+                 attribute_groups=[], data=None, request=None, length=sys.maxint):
+        """
+        Create a Request.  Takes either the segments of the request
+        separately, or a file handle for the request to parse.  If the
+        file handle is passed in, all other arguments are ignored.
+
+        Keyword arguments for passing in the segments of the request:
+        
+            version -- a tuple of two signed chars, identifying the
+                       major version and minor version numbers of the
+                       request
+                            
+            operation_id -- a signed short, identifying the id of the
+                            requested operation
+
+            request_id -- a signed int, identifying the id of the
+                          request itself.
+
+            attribute_groups -- a list of Attributes, at least length 1
+
+            data -- (optional) variable length, containing the actual
+                    data of the request
+
+        Keyword arguments for passing in the raw request:
+
+            request -- a file handle that supports the read()
+                       operation
+        """
+
+        if request is None:
+            # make sure the version number isn't empty
+            assert version is not None
+            # make sure verison is a tuple of length 2
+            assert isinstance(version, tuple)
+            assert len(version) == 2
+            # make sure the operation id isn't empty
+            assert operation_id is not None
+            # make sure the request id isn't empty
+            assert request_id is not None
+            # make sure attribute_groups is a list of Attributes
+            for a in attribute_groups: assert isinstance(a, AttributeGroup)
+            
+        # if the request isn't None, then we'll read directly from
+        # that file handle
+        if request is not None:
+            # read the version-number (two signed chars)
+            self.version        = struct.unpack('>bb', request.read(2))
+            length -= 2
+            logger.debug("version-number : (0x%X, 0x%X)" % self.version)
+
+            # read the operation-id (or status-code, but that's only
+            # for a response) (signed short)
+            self.operation_id   = struct.unpack('>h', request.read(2))[0]
+            length -= 2
+            logger.debug("operation-id : 0x%X" % self.operation_id)
+
+            # read the request-id (signed int)
+            self.request_id     = struct.unpack('>i', request.read(4))[0]
+            length -= 4
+            logger.debug("request-id : 0x%X" % self.request_id)
+
+            # now we have to read in the attributes.  Each attribute
+            # has a tag (1 byte) and a sequence of values (n bytes)
+            self.attribute_groups = []
+
+            # read in the next byte
+            next_byte = struct.unpack('>b', request.read(1))[0]
+            length -=1
+            logger.debug("next byte : 0x%X" % next_byte)
+
+            # as long as the next byte isn't signaling the end of the
+            # attributes, keep looping and parsing attributes
+            while next_byte != AttributeTags.END:
+                
+                attribute_group_tag = next_byte
+                logger.debug("attribute-tag : %i" % attribute_group_tag)
+
+                attributes = []
+
+                next_byte = struct.unpack('>b', request.read(1))[0]
+                length -= 1
+                logger.debug("next byte : 0x%X" % next_byte)
+
+                while next_byte > 0x0F:
+                    
+                    # read in the value tag (signed char)
+                    value_tag     = next_byte
+                    logger.debug("value-tag : 0x%X" % value_tag)
+                    
+                    # read in the length of the name (signed short)
+                    name_length   = struct.unpack('>h', request.read(2))[0]
+                    length -= 2
+                    logger.debug("name-length : %i" % name_length)
+                    
+                    if name_length != AttributeTags.ZERO_NAME_LENGTH:
+                        # read the name (a string of name_length bytes)
+                        name          = request.read(name_length)
+                        length -= name_length
+                        logger.debug("name : %s" % name)
+                    
+                        # read in the length of the value (signed short)
+                        value_length  = struct.unpack('>h', request.read(2))[0]
+                        length -= 2
+                        logger.debug("value-length : %i" % value_length)
+                    
+                        # read in the value (string of value_length bytes)
+                        value         = request.read(value_length)
+                        length -= value_length
+                        
+                        ippvalue = Value.unpack(value_tag, value)
+                        logger.debug("value : %s" % ippvalue.value)
+
+                        # create a new Attribute from the data we just
+                        # read in, and add it to our attributes list
+                        attributes.append(Attribute(name, [ippvalue]))
+
+                    else:
+                        # read in the length of the value (signed short)
+                        value_length  = struct.unpack('>h', request.read(2))[0]
+                        length -= 2
+                        logger.debug("value-length : %i" % value_length)
+                    
+                        # read in the value (string of value_length bytes)
+                        value         = request.read(value_length)
+                        length -= value_length
+
+                        ippvalue = Value.unpack(value_tag, value)
+                        logger.debug("value : %s" % ippvalue.value)
+
+                        # add another value to the last attribute
+                        attributes[-1].values.append(ippvalue)
+
+                    # read another byte
+                    next_byte = struct.unpack('>b', request.read(1))[0]
+                    length -= 1
+
+                self.attribute_groups.append(AttributeGroup(
+                    attribute_group_tag, attributes))
+
+            # once we hit the end-of-attributes tag, the only thing
+            # left is the data, so go ahead and read all of it
+            assert length >= 0
+            self.data = request.read(length)
+            logger.debug("data : %s" % self.data)
+
+        # otherwise, just set the class variables to the keyword
+        # arguments passed in
+        else:
+            self.version = (version[0], version[1])
+            self.operation_id = operation_id
+            self.request_id = request_id
+            self.attribute_groups = attribute_groups
+            self.data = data
+
+    def getAttributeGroup(self, tag):
+        return filter(lambda x: x.attribute_group_tag == tag,
+                      self.attribute_groups)
+
+    @property
+    def packed_value(self):
+        """
+        Packs the value data into binary data.
+        """
+
+        # make sure the version number isn't empty
+        assert self.version is not None
+        # make sure verison is a tuple of length 2
+        assert isinstance(self.version, tuple)
+        assert len(self.version) == 2
+        # make sure the operation id isn't empty
+        assert self.operation_id is not None
+        # make sure the request id isn't empty
+        assert self.request_id is not None
+        # make sure attribute_groups is a list of Attributes
+        assert len(self.attribute_groups) > 0
+        for a in self.attribute_groups: assert isinstance(a, AttributeGroup)
+
+        # convert the version, operation id, and request id to binary
+        preattributes = struct.pack('>bbhi',
+                                    self.version[0],
+                                    self.version[1],
+                                    self.operation_id,
+                                    self.request_id)
+
+        # convert the attribute groups to binary
+        attribute_groups = ''.join([a.packed_value for a in self.attribute_groups])
+
+        # conver the end-of-attributes-tag to binary
+        end_of_attributes_tag = struct.pack('>b', AttributeTags.END)
+
+        # convert the data to binary
+        if self.data is not None:
+            data = ''.join([struct.pack('>b', x) for x in self.data])
+        else:
+            data = ''
+
+        # append everything together and return it
+        return preattributes + attribute_groups + end_of_attributes_tag + data
+
+    def __repr__(self):
+        return '<IPPRequest (version=%r, operation_id=%x, request_id=%r, attribute_groups=%r)>' % (self.version, self.operation_id, self.request_id, self.attribute_groups)
Index: server/lib/gutenbach/ipp/value.py
===================================================================
--- server/lib/gutenbach/ipp/value.py	(revision df510617607a17049c40187f93c7ce3d26508ae1)
+++ server/lib/gutenbach/ipp/value.py	(revision df510617607a17049c40187f93c7ce3d26508ae1)
@@ -0,0 +1,302 @@
+#!/usr/bin/python
+
+import sys, struct, logging
+from .constants import OutOfBandTags, IntegerTags, OctetStringTags, CharacterStringTags
+
+# initialize logger
+logger = logging.getLogger(__name__)
+
+def setter(prop):
+    def f(func):
+        return property(prop.fget, func, prop.fdel, prop.__doc__)
+    return f
+
+class Value(object):
+    """
+    An IPP value consists of a tag and a value.
+
+    From RFC 2565:
+     -----------------------------------------------
+     |                   value-tag                 |   1 byte
+     -----------------------------------------------
+     |            name-length  (value is 0x0000)   |   2 bytes
+     -----------------------------------------------
+     |              value-length  (value is v)     |   2 bytes
+     -----------------------------------------------
+     |                     value                   |   v bytes
+     -----------------------------------------------    
+    """
+
+    def __init__(self, value_tag=None, value=None):
+        """
+        Initialize a Value.  There are three different ways you can
+        call this method:
+
+            Value() -- creates an empty Value instance
+
+            Value(value_tag, value) -- creates a Value instance from
+            a non-binary value
+
+        If you create an empty Value instance, once you have set
+        value_tag and value, you can retrieve the packed value from
+        the packed_value property.
+
+        Arguments:
+
+            value_tag -- one byte, identifying the type of value
+
+            value -- variable size, containing the actual value.
+            It should be a string or number.
+        """
+
+        # make sure the arguments are valid
+        if value is not None:
+            assert value_tag is not None, \
+                   "value_tag must not be null because " + \
+                   "value is not null!"
+
+        # initialize member variables
+        self.value_tag = value_tag # one byte, the type of value
+        self.value     = value     # non-binary value of self.value
+
+    @classmethod
+    def unpack(cls, value_tag, packed_value):
+        """Unpack a binary IPP value
+
+        Unpacks a binary string into a Value object.
+
+        """
+        return cls(value_tag, cls._unpack(value_tag, packed_value))
+
+    @staticmethod
+    def _unpack(value_tag, packed_value):
+        """
+        Given self.value_tag and self.packed_value, unpack the binary
+        value into either a string or number.  These values MUST NOT
+        be null.
+
+        Returns: unpacked value
+
+        """
+
+        assert value_tag is not None, \
+               "Cannot unpack values with unspecified value tag!"
+        assert packed_value is not None, \
+               "Cannot unpack null values!"
+
+        value = None
+
+        # out-of-band value tags
+        if value_tag == OutOfBandTags.UNSUPPORTED or \
+               value_tag == OutOfBandTags.DEFAULT or \
+               value_tag == OutOfBandTags.UNKNOWN or \
+               value_tag == OutOfBandTags.NO_VALUE:
+            value_size = 0
+            value = ''
+
+        # integer value tags
+        elif value_tag == IntegerTags.INTEGER:
+            value = struct.unpack('>i', packed_value)[0]
+        elif value_tag == IntegerTags.BOOLEAN:
+            value = struct.unpack('>b', packed_value)[0]
+        elif value_tag == IntegerTags.ENUM:
+            value = struct.unpack('>i', packed_value)[0]
+
+        
+        elif value_tag == OctetStringTags.DATETIME:
+            # field  octets  contents                  range
+            # -----  ------  --------                  -----
+            #   1      1-2   year                      0..65536
+            #   2       3    month                     1..12
+            #   3       4    day                       1..31
+            #   4       5    hour                      0..23
+            #   5       6    minutes                   0..59
+            #   6       7    seconds                   0..60
+            #                (use 60 for leap-second)
+            #   7       8    deci-seconds              0..9
+            #   8       9    direction from UTC        '+' / '-'
+            #   9      10    hours from UTC            0..11
+            #  10      11    minutes from UTC          0..59
+
+            value = struct.unpack('>hbbbbbbcbb', packed_value)
+            
+        elif value_tag == OctetStringTags.RESOLUTION:
+            # OCTET-STRING consisting of nine octets of 2
+            # SIGNED-INTEGERs followed by a SIGNED-BYTE. The first
+            # SIGNED-INTEGER contains the value of cross feed
+            # direction resolution. The second SIGNED-INTEGER contains
+            # the value of feed direction resolution. The SIGNED-BYTE
+            # contains the units
+
+            value = struct.unpack('>iib', packed_value)
+            
+        elif value_tag == OctetStringTags.RANGE_OF_INTEGER:
+            # Eight octets consisting of 2 SIGNED-INTEGERs.  The first
+            # SIGNED-INTEGER contains the lower bound and the second
+            # SIGNED-INTEGER contains the upper bound.
+
+            value = struct.unpack('>ii', packed_value)
+
+        elif value_tag == OctetStringTags.TEXT_WITH_LANGUAGE or \
+                 value_tag == OctetStringTags.NAME_WITH_LANGUAGE:
+            a = struct.unpack('>h', packed_value[:2])[0]
+            b = struct.unpack('>%ss' % a, packed_value[2:a+2])[0]
+            c = struct.unpack('>h', packed_value[a+2:a+4])[0]
+            d = struct.unpack('>%ss' % c, packed_value[a+4:][0])
+            value = (a, b, c, d)
+
+        # character string value tags
+        elif value_tag == \
+                 CharacterStringTags.TEXT_WITHOUT_LANGUAGE or \
+                 value_tag == \
+                 CharacterStringTags.NAME_WITHOUT_LANGUAGE:
+            value = str(packed_value)
+        elif value_tag == CharacterStringTags.GENERIC or \
+                 value_tag == CharacterStringTags.KEYWORD or \
+                 value_tag == CharacterStringTags.URI or \
+                 value_tag == CharacterStringTags.URI_SCHEME or \
+                 value_tag == CharacterStringTags.CHARSET or \
+                 value_tag == CharacterStringTags.NATURAL_LANGUAGE or \
+                 value_tag == CharacterStringTags.MIME_MEDIA_TYPE:
+            value = str(packed_value)
+
+        # anything else that we didn't handle
+        else:
+            if value is None:
+                value = packed_value
+
+        return value
+
+    @property
+    def packed_value(self):
+        """
+        Given self.value_tag and self.value, pack the value into
+        binary form.  These values MUST NOT be null.
+
+        Returns: packed_value
+
+        """
+        
+        assert self.value_tag is not None, \
+               "cannot pack value with null value tag!"
+        assert self.value is not None, \
+               "cannot pack null value!"
+
+        packed_value = None
+
+        # out-of-band value tags
+        if self.value_tag == OutOfBandTags.UNSUPPORTED or \
+               self.value_tag == OutOfBandTags.DEFAULT or \
+               self.value_tag == OutOfBandTags.UNKNOWN or \
+               self.value_tag == OutOfBandTags.NO_VALUE:
+            packed_value = ''
+
+        # integer value tags
+        elif self.value_tag == IntegerTags.INTEGER:
+            packed_value = struct.pack('>i', self.value)
+        elif self.value_tag == IntegerTags.BOOLEAN:
+            packed_value = struct.pack('>b', self.value)
+        elif self.value_tag == IntegerTags.ENUM:
+            packed_value = struct.pack('>i', self.value)
+
+        # octet string value tags
+        elif self.value_tag == OctetStringTags.DATETIME:
+            # field  octets  contents                  range
+            # -----  ------  --------                  -----
+            #   1      1-2   year                      0..65536
+            #   2       3    month                     1..12
+            #   3       4    day                       1..31
+            #   4       5    hour                      0..23
+            #   5       6    minutes                   0..59
+            #   6       7    seconds                   0..60
+            #                (use 60 for leap-second)
+            #   7       8    deci-seconds              0..9
+            #   8       9    direction from UTC        '+' / '-'
+            #   9      10    hours from UTC            0..11
+            #  10      11    minutes from UTC          0..59
+
+            packed_value = struct.pack('>hbbbbbbcbb', *self.value)
+            
+        elif self.value_tag == OctetStringTags.RESOLUTION:
+            # OCTET-STRING consisting of nine octets of 2
+            # SIGNED-INTEGERs followed by a SIGNED-BYTE. The first
+            # SIGNED-INTEGER contains the value of cross feed
+            # direction resolution. The second SIGNED-INTEGER contains
+            # the value of feed direction resolution. The SIGNED-BYTE
+            # contains the units
+
+            packed_value = truct.pack('>iib', *self.value)
+            
+        elif self.value_tag == OctetStringTags.RANGE_OF_INTEGER:
+            # Eight octets consisting of 2 SIGNED-INTEGERs.  The first
+            # SIGNED-INTEGER contains the lower bound and the second
+            # SIGNED-INTEGER contains the upper bound.
+
+            packed_value = struct.pack('>ii', *self.value)
+
+        elif self.value_tag == OctetStringTags.TEXT_WITH_LANGUAGE or \
+                 self.value_tag == OctetStringTags.NAME_WITH_LANGUAGE:
+            
+            a_bin = struct.pack('>h', self.value[0])
+            b_bin = struct.pack('>%ss' % self.value[0], self.value[1])
+            c_bin = struct.pack('>h', self.value[2])
+            d_bin = struct.pack('>%ss' % self.value[2], self.value[3])
+
+            packed_value = a_bin + b_bin + c_bin + d_bin
+
+        # character string value tags
+        elif self.value_tag == \
+                 CharacterStringTags.TEXT_WITHOUT_LANGUAGE or \
+                 self.value_tag == \
+                 CharacterStringTags.NAME_WITHOUT_LANGUAGE:
+
+            packed_value = struct.pack('>%ss' % len(self.value),
+                                       self.value)
+                    
+        elif self.value_tag == CharacterStringTags.GENERIC or \
+                 self.value_tag == CharacterStringTags.KEYWORD or \
+                 self.value_tag == CharacterStringTags.URI or \
+                 self.value_tag == CharacterStringTags.URI_SCHEME or \
+                 self.value_tag == CharacterStringTags.CHARSET or \
+                 self.value_tag == CharacterStringTags.NATURAL_LANGUAGE or \
+                 self.value_tag == CharacterStringTags.MIME_MEDIA_TYPE:
+            
+            packed_value = struct.pack('>%ss' % len(self.value),
+                                       self.value)
+
+        else:
+            packed_value = self.value
+
+        return packed_value
+
+    @setter(packed_value)
+    def packed_value(self, packed_value):
+        """Replace a value using a new packed value
+
+        Unpacks a new packed_value (of the same value_tag).
+
+        """
+        self.value = self._unpack(self.value_tag, packed_value)
+
+    @property
+    def packed_value_size(self):
+        """
+        Get the size of the value in bytes.
+        """
+        
+        return len(self.packed_value)
+
+    @property
+    def total_size(self):
+        """
+        Get the total size of the IPP value.
+        """
+
+        # 1 byte for the tag
+        return self.packed_value_size + 1
+
+    def __str__(self):
+        return str(self.value)
+
+    def __repr__(self):
+        return '<IPPValue (%x, %r)>' % (self.value_tag, self.value)
Index: server/lib/gutenbach/server/__init__.py
===================================================================
--- server/lib/gutenbach/server/__init__.py	(revision df510617607a17049c40187f93c7ce3d26508ae1)
+++ server/lib/gutenbach/server/__init__.py	(revision df510617607a17049c40187f93c7ce3d26508ae1)
@@ -0,0 +1,314 @@
+#!/usr/bin/python
+
+import logging, BaseHTTPServer
+import time
+import ipp
+import ipp.constants as const
+from ipp.constants import Operations
+
+logging.basicConfig(level=logging.DEBUG)
+
+def handler_for(operation):
+    def f(func):
+        func.ipp_operation = operation
+        return func
+    return f
+
+class IPPRequestHandler(object):
+    def _ipp_dispatch(self, request):
+        for d in dir(self):
+            if getattr(getattr(self, d), "ipp_operation", None) == request.operation_id:
+                return getattr(self, d)
+        return self.unknown_operation
+
+    def unknown_operation(self, request, response):
+        print "Received UNKNOWN OPERATION %x" % request.operation_id
+        response.operation_id = const.StatusCodes.OPERATION_NOT_SUPPORTED
+
+class PrinterRequestHandler(IPPRequestHandler):
+    def __init__(self, name):
+        self.name = name
+
+    @handler_for(Operations.GET_PRINTER_ATTRIBUTES)
+    def get_printer_attributes(self, request, response):
+        printer_attributes = ipp.AttributeGroup(const.AttributeTags.PRINTER)
+        printer_attributes[:] = \
+            ipp.Attribute("printer-uri-supported",
+                          [ipp.Value(ipp.Tags.URI,
+                                     "ipp://localhost:8000/printers/"+self.name)]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("uri-authentication-supported",
+                          [ipp.Value(ipp.Tags.KEYWORD,
+                                     "none")]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("uri-security-supported",
+                          [ipp.Value(ipp.Tags.KEYWORD,
+                                     "none")]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("printer-name",
+                          [ipp.Value(ipp.Tags.NAME_WITHOUT_LANGUAGE,
+                                     self.name)]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("printer-state",
+                          [ipp.Value(ipp.Tags.ENUM,
+                                     const.PrinterStates.IDLE)]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("printer-state-reasons",
+                          [ipp.Value(ipp.Tags.KEYWORD,
+                                     "none")]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("ipp-versions-supported",
+                          [ipp.Value(ipp.Tags.KEYWORD,
+                                     "1.0"),
+                           ipp.Value(ipp.Tags.KEYWORD,
+                                     "1.1")]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("operations-supported",
+                          [ipp.Value(ipp.Tags.ENUM,
+                                     Operations.GET_JOBS)]
+                          ) #XXX: We should query ourself for the supported operations
+        printer_attributes[:] = \
+            ipp.Attribute("charset-configured",
+                          [ipp.Value(ipp.Tags.CHARSET,
+                                     "utf-8")]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("charset-supported",
+                          [ipp.Value(ipp.Tags.CHARSET,
+                                     "utf-8")]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("natural-language-configured",
+                          [ipp.Value(ipp.Tags.NATURAL_LANGUAGE,
+                                     "en-us")]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("generated-natural-language-supported",
+                          [ipp.Value(ipp.Tags.NATURAL_LANGUAGE,
+                                     "en-us")]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("document-format-default",
+                          [ipp.Value(ipp.Tags.MIME_MEDIA_TYPE,
+                                     "application/octet-stream")]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("document-format-supported",
+                          [ipp.Value(ipp.Tags.MIME_MEDIA_TYPE,
+                                     "application/octet-stream"),
+                           ipp.Value(ipp.Tags.MIME_MEDIA_TYPE,
+                                     "audio/mp3")]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("printer-is-accepting-jobs",
+                          [ipp.Value(ipp.Tags.BOOLEAN,
+                                     True)]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("queued-job-count",
+                          [ipp.Value(ipp.Tags.INTEGER,
+                                     1)]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("pdl-override-supported",
+                          [ipp.Value(ipp.Tags.KEYWORD,
+                                     "not-attempted")]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("printer-up-time",
+                          [ipp.Value(ipp.Tags.INTEGER,
+                                     int(time.time()))]
+                          )
+        printer_attributes[:] = \
+            ipp.Attribute("compression-supported",
+                          [ipp.Value(ipp.Tags.KEYWORD,
+                                     "none")]
+                          )
+
+        response.attribute_groups.append(printer_attributes)
+        response.operation_id = const.StatusCodes.OK
+        print "get_printer_attributes called"
+
+class RootRequestHandler(IPPRequestHandler):
+    printers = [PrinterRequestHandler(name="sipbmp3")]
+
+    @handler_for(Operations.CUPS_GET_DEFAULT)
+    def cups_get_default(self, request, response):
+        print "get_default called"
+        return self.printers[0].get_printer_attributes(request, response)
+
+    @handler_for(Operations.CUPS_GET_PRINTERS)
+    def cups_get_printers(self, request, response):
+        print "get_printers called"
+        response.operation_id = const.StatusCodes.OK
+        for p in self.printers:
+            # Each printer will append a new printer attribute group.
+            p.get_printer_attributes(request, response)
+
+    @handler_for(Operations.CUPS_GET_CLASSES)
+    def cups_get_classes(self, request, response):
+        print "get_classes called"
+        response.operation_id = const.StatusCodes.OK
+        # We have no printer classes, so nothing to return.
+
+class GutenbachIPPHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+    def setup(self):
+        self.root = RootRequestHandler()
+        BaseHTTPServer.BaseHTTPRequestHandler.setup(self)
+
+    def handle_one_request(self):
+        self.raw_requestline = self.rfile.readline()
+        if not self.raw_requestline:
+            self.close_connection = 1
+            return
+        if not self.parse_request(): # An error code has been sent, just exit
+            return
+        self.handle_ipp()
+
+    def handle_ipp(self):
+        length = int(self.headers.getheader('content-length', 0))
+        request = ipp.Request(request=self.rfile,
+                              length=length)
+        print "Received request:", repr(request)
+
+        response_kwargs = {}
+        response_kwargs['version'] = request.version
+        response_kwargs['operation_id'] = const.StatusCodes.INTERNAL_ERROR
+        response_kwargs['request_id'] = request.request_id
+        response_kwargs['attribute_groups'] = [
+            ipp.AttributeGroup(const.AttributeTags.OPERATION,
+                               [ipp.Attribute('attributes-charset',
+                                              [ipp.Value(ipp.Tags.CHARSET,
+                                                         'utf-8'
+                                                         )]),
+                                ipp.Attribute('attributes-natural-language',
+                                              [ipp.Value(ipp.Tags.NATURAL_LANGUAGE,
+                                                         'en-us'
+                                                         )])
+                                ])]
+
+        response = ipp.Request(**response_kwargs)
+        handler = self.root._ipp_dispatch(request)
+        handler(request, response)
+        print "Sending response:", repr(response)
+
+        self.send_response(200, "o hai")
+        self.send_header("Content-Type", "application/ipp")
+        self.send_header("Connection", "close")
+        self.end_headers()
+        self.wfile.write(response.packed_value)
+
+    def get_jobs(self, request, response_kwargs):
+        """get-jobs response"""
+
+        job_attributes = [ipp.Attribute('job-id',
+                                       [ipp.Value(ipp.Tags.INTEGER,
+                                                 12345,
+                                                 )]),
+                          ipp.Attribute('job-name',
+                                       [ipp.Value(ipp.Tags.NAME_WITHOUT_LANGUAGE,
+                                                 'foo',
+                                                 )]),
+                          ipp.Attribute('job-originating-user-name',
+                                       [ipp.Value(ipp.Tags.NAME_WITHOUT_LANGUAGE,
+                                                 'jhamrick',
+                                                 )]),
+                          ipp.Attribute('job-k-octets',
+                                       [ipp.Value(ipp.Tags.INTEGER,
+                                                 1,
+                                                 )]),
+                          ipp.Attribute('job-state',
+                                       [ipp.Value(ipp.Tags.ENUM,
+                                                 const.JobStates.HELD,
+                                                 )]),
+                          ipp.Attribute('job-printer-uri',
+                                       [ipp.Value(ipp.Tags.URI,
+                                                 'ipp://localhost:8000/printers/foo',
+                                                 )])]
+
+
+        #req_op_attributes = request.getAttributeGroup(ipp.Tags.OPERATION_ATTRIBUTES_TAG)
+        #print req_op_attributes
+        #printer_uri = req_op_attributes[0].getAttribute('printer-uri')
+
+        op_attributes = [ipp.Attribute('attributes-charset',
+                                      [ipp.Value(ipp.Tags.CHARSET,
+                                                'utf-8',
+                                                )]),
+                         ipp.Attribute('attributes-natural-language',
+                                      [ipp.Value(ipp.Tags.NATURAL_LANGUAGE,
+                                                'en-us',
+                                                )])]
+        
+        job_attribute_group = ipp.AttributeGroup(const.AttributeTags.JOB,
+                                                 job_attributes)
+        op_attributes_group = ipp.AttributeGroup(const.AttributeTags.OPERATION,
+                                                 op_attributes)
+        response_kwargs['attribute_groups'] = [op_attributes_group,job_attribute_group]
+        response_kwargs['operation_id'] = const.StatusCodes.OK
+
+        return response_kwargs
+
+    ##### Printer Commands
+
+    def print_job(self, request):
+        pass
+
+    def validate_job(self, request):
+        pass
+
+    #def get_jobs(self, request):
+    #    pass
+
+    def print_uri(self, request):
+        pass
+
+    def create_job(self, request):
+        pass
+
+    def pause_printer(self, request):
+        pass
+
+    def resume_printer(self, request):
+        pass
+
+    def set_printer_attributes(self, request):
+        pass
+
+    ##### Job Commands
+
+    def cancel_job(self, request):
+        pass
+
+    def get_job_attributes(self, request):
+        pass
+
+    def send_document(self, request):
+        pass
+
+    def send_uri(self, request):
+        pass
+
+    def set_job_attributes(self, request):
+        pass
+
+    def cups_get_document(self, request):
+        pass
+
+    def restart_job(self, request):
+        pass
+
+    def promote_job(self, request):
+        pass
+
+if __name__ == '__main__':
+    server_address = ('', 8000)
+    httpd = BaseHTTPServer.HTTPServer(server_address, GutenbachIPPHandler)
+    httpd.serve_forever()
Index: server/lib/gutenbach/server/exceptions.py
===================================================================
--- server/lib/gutenbach/server/exceptions.py	(revision df510617607a17049c40187f93c7ce3d26508ae1)
+++ server/lib/gutenbach/server/exceptions.py	(revision df510617607a17049c40187f93c7ce3d26508ae1)
@@ -0,0 +1,13 @@
+class InvalidJobException(Exception):
+    def __init__(self, jobid):
+	self.jobid = jobid
+
+    def __str__(self):
+	return "Job with id '%d' does not exist!" % self.jobid
+
+class InvalidPrinterStateException(Exception):
+    def __init__(self, message):
+	self.message = message
+
+    def __str__(self):
+	return self.message
Index: server/lib/gutenbach/server/job.py
===================================================================
--- server/lib/gutenbach/server/job.py	(revision df510617607a17049c40187f93c7ce3d26508ae1)
+++ server/lib/gutenbach/server/job.py	(revision df510617607a17049c40187f93c7ce3d26508ae1)
@@ -0,0 +1,81 @@
+import os
+from exceptions import InvalidJobException, \
+     InvalidPrinterStateException
+
+class Job(object):
+
+    def __init__(self, document=None):
+	"""Initialize a Gutenbach job.
+
+	This sets the status to 'initializing' and optionally sets the
+	document to print to the value of document.
+	"""
+	 
+	self._jobid = None
+	self._status = 'initializing'
+	self._document = document
+	self._printer = None
+
+    @property
+    def jobid(self):
+	return self._jobid
+
+    @jobid.setter
+    def jobid(self, val):
+	raise AttributeError("Setting jobid is illegal!")
+
+    @property
+    def document(self):
+	return self._document
+
+    @document.setter
+    def document(self, path):
+	if not os.path.exists(path):
+	    raise IOError("Document '%s' does not exist!" % path)
+	self._document = path
+
+    @property
+    def status(self):
+	return self._status
+
+    @status.setter
+    def status(self, val):
+	raise AttributeError(
+	    "Setting status directly is illegal!  " + \
+	    "Please use enqueue(), play(), or finish().")
+
+    @property
+    def printer(self):
+	return self._printer
+
+    @printer.setter
+    def printer(self, val):
+	raise AttributeError(
+	    "Setting printer directly is illegal!  " + \
+	    "Please use enqueue().")
+
+    def enqueue(self, printer):
+	if self.status != 'initializing':
+	    raise InvalidJobException(
+		"Cannot enqueue a job that has " + \
+		"already been initialized!")
+	self._printer = printer
+	self._status = 'active'
+
+    def play(self):
+	if self.status != 'active':
+	    raise InvalidJobException(
+		"Cannot play an inactive job!")
+	
+	self._status = 'playing'
+	# TODO: add external call to music player
+	self.printer.complete_job(self.jobid)
+
+    def finished(self):
+	self._status = 'finished'
+
+    def __repr__(self):
+	return str(self)
+
+    def __str__(self):
+	return "<Job %d>" % self.jobid
Index: server/lib/gutenbach/server/printer.py
===================================================================
--- server/lib/gutenbach/server/printer.py	(revision df510617607a17049c40187f93c7ce3d26508ae1)
+++ server/lib/gutenbach/server/printer.py	(revision df510617607a17049c40187f93c7ce3d26508ae1)
@@ -0,0 +1,80 @@
+import ipp
+import alsaaudio as aa
+from ipp.attribute import Attribute
+from .exceptions import InvalidJobException, \
+     InvalidPrinterStateException
+
+class Printer(object):
+
+    def __init__(self, name, card, mixer):
+
+	self.name = name
+
+	if card >= len(aa.cards()):
+	    raise aa.ALSAAudioError(
+		"Audio card at index %d does not exist!" % card)
+	elif mixer not in aa.mixers(card):
+	    raise aa.ALSAAudioError(
+		"Audio mixer '%s' does not exist!" % mixer)
+	
+	self.card = card
+	self.mixer = mixer
+
+	self.finished_jobs = []
+	self.active_jobs = []
+	self.jobs = {}
+
+	self._next_jobid = -1
+
+    @property
+    def next_jobid(self):
+	self._next_jobid += 1
+	return self._next_jobid
+
+    @next_jobid.setter
+    def next_jobid(self, val):
+	raise AttributeError("Setting next_jobid is illegal!")
+
+    def print_job(self, job):
+	jobid = self.next_jobid
+	self.active_jobs.append(jobid)
+	self.jobs[jobid] = job
+	job.jobid = jobid
+	job.enqueue(self)
+	return jobid
+
+    def complete_job(self, jobid):
+	job = self.active_jobs.pop(0)
+	if job.jobid != jobid:
+	    raise InvalidJobException(
+		"Completed job %d has unexpected job id %d!" % \
+		(job.jobid, jobid))
+	
+	self.finished_jobs.append(job)
+	job.finish()
+	return job.jobid
+
+    def start_job(self, jobid):
+	job = self.active_jobs[0]
+	if job.jobid != jobid:
+	    raise InvalidJobException(
+		"Completed job %d has unexpected job id %d!" % \
+		(job.jobid, jobid))
+
+	if job.status == 'playing':
+	    raise InvalidPrinterStateException(
+		"Next job in queue (id %d) is " + \
+		"already playing!" % jobid)
+
+	job.play()
+
+    def get_job(self, jobid):
+	if jobid not in self.jobs:
+	    raise InvalidJobException(jobid)
+	return self.jobs[jobid]
+
+    def __repr__(self):
+	return str(self)
+
+    def __str__(self):
+	return "<Printer '%s'>" % self.name
Index: rver/lib/ipp/__init__.py
===================================================================
--- server/lib/ipp/__init__.py	(revision 91abb7fec8a1f6995d6c5f691039529c42c22894)
+++ 	(revision )
@@ -1,5 +1,0 @@
-from attribute import Attribute
-from attributegroup import AttributeGroup
-from request import Request
-from value import Value
-from constants import Tags
Index: rver/lib/ipp/attribute.py
===================================================================
--- server/lib/ipp/attribute.py	(revision 7a1c039c27cd8beefe98c73143f72e3cbaa8b79e)
+++ 	(revision )
@@ -1,155 +1,0 @@
-#!/usr/bin/python
-
-import sys, struct, logging
-from .value import Value
-
-# initialize logger
-logger = logging.getLogger(__name__)
-
-class Attribute(object):
-    """
-    In addition to what the RFC reports, an attribute has an
-    'attribute tag', which specifies what type of attribute it is.
-    It is 1 bytes long, and comes before the list of values.
-
-    From RFC 2565:
-
-    Each attribute consists of:
-    -----------------------------------------------
-    |                   value-tag                 |   1 byte
-    -----------------------------------------------
-    |               name-length  (value is u)     |   2 bytes
-    -----------------------------------------------
-    |                     name                    |   u bytes
-    -----------------------------------------------
-    |              value-length  (value is v)     |   2 bytes
-    -----------------------------------------------
-    |                     value                   |   v bytes
-    -----------------------------------------------
-
-    An additional value consists of:
-    -----------------------------------------------------------
-    |                   value-tag                 |   1 byte  |
-    -----------------------------------------------           |
-    |            name-length  (value is 0x0000)   |   2 bytes |
-    -----------------------------------------------           |-0 or more
-    |              value-length (value is w)      |   2 bytes |
-    -----------------------------------------------           |
-    |                     value                   |   w bytes |
-    -----------------------------------------------------------
-    """
-
-    def __init__(self, name=None, values=None):
-        """
-        Initialize an Attribute.  This function can be called in three
-        different ways:
-
-            Attribute() -- creates an empty Attribute
-
-            Attribute(name) -- creates an empty Attribute with a name
-
-            Attribute(name, values) -- creates an Attribute
-            initialized with a name and list of values
-        
-        Arguments:
-
-            name -- the name of the attribute
-
-            values -- a list of Values.  May not be empty.
-        """
-
-        if name is not None:
-            assert isinstance(name, str), \
-                   "Attribute name must be a string!"
-        if values is None:
-            values = []
-        for value in values:
-            assert isinstance(value, Value), \
-                   "Value %r must be of type Value" % (value)
-
-        self.name = name
-        self.values = values
-
-    @property
-    def packed_value(self):
-        """
-        Packs the attribute data into binary data.
-        """
-
-        assert self.name is not None, \
-               "cannot pack unnamed attribute!"
-        assert len(self.values) > 0, \
-               "cannot pack empty attribute!"
-
-        # get the binary data for all the values
-        values = []
-        for v, i in zip(self.values, xrange(len(self.values))):
-
-            # get the name length (0 for everything but the first
-            # value)
-            if i == 0:
-                name_length = len(self.name)
-            else:
-                name_length = 0
-
-            # get the value length and binary value
-            value_bin = v.packed_value
-            value_length = len(value_bin)
-
-            logger.debug("dumping name_length : %i" % name_length)
-            logger.debug("dumping name : %s" % self.name)
-            logger.debug("dumping value_length : %i" % value_length)
-            logger.debug("dumping value : %s" % v.value)
-
-            # the value tag in binary
-            value_tag_bin = struct.pack('>b', v.value_tag)
-
-            # the name length in binary
-            name_length_bin = struct.pack('>h', name_length)
-
-            # the name in binary
-            name_bin = self.name
-
-            # the value length in binary
-            value_length_bin = struct.pack('>h', value_length)
-
-            if i == 0:
-                values.append(''.join([value_tag_bin,
-                                       name_length_bin,
-                                       name_bin,
-                                       value_length_bin,
-                                       value_bin]))
-            else:
-                values.append(''.join([value_tag_bin,
-                                       name_length_bin,
-                                       value_length_bin,
-                                       value_bin]))
-
-        # concatenate everything together and return it along with the
-        # total length of the attribute
-        return ''.join(values)
-
-    @property
-    def packed_value_size(self):
-        """
-        Gets the total size of the attribute.
-        """
-        return len(self.packed_value)
-
-    total_size = packed_value_size
-
-    def __str__(self):
-        if len(self.values) > 0:
-            values = [str(v) for v in self.values]
-        else:
-            values = "None"
-
-        if self.name is None:
-            name = "None"
-        else:
-            name = self.name
-        
-        return "%s: %s" % (name, str(values))
-
-    def __repr__(self):
-        return '<IPPAttribute (%r, %r)>' % (self.name, self.values)
Index: rver/lib/ipp/attributegroup.py
===================================================================
--- server/lib/ipp/attributegroup.py	(revision 776a659edf6a8f91d1a6fb6f009e4abac41bf064)
+++ 	(revision )
@@ -1,109 +1,0 @@
-#!/usr/bin/python
-
-import sys, struct, logging
-from .attribute import Attribute
-
-# initialize logger
-logger = logging.getLogger(__name__)
-
-class AttributeGroup(object):
-    """
-    An AttributeGroup consists of an attribute-group-tag, followed by
-    a sequence of Attributes. According to RFC 2565, "Within an
-    attribute-sequence, if two attributes have the same name, the
-    first occurrence MUST be ignored.", so we can effectively treat
-    this as an ordered dictionary.
-    """
-
-    def __init__(self, attribute_group_tag=None, attributes=[]):
-        """
-        Initialize an AttributeGroup.  An AttributeGroup can be
-        initialized in three ways:
-
-            AttributeGroup()
-            AttributeGroup(attribute_group_tag)
-            AttributeGroup(attribute_group_tag, attributes)
-
-        Arguments:
-
-            attribute_group_tag -- a signed char, holds the tag of the
-                                   attribute group
-
-            attributes -- a list of attributes
-        """
-
-        if attribute_group_tag is not None:
-            assert isinstance(attribute_group_tag, int), \
-                   "attribute_group_tag must be a character!"
-            
-
-        self.attribute_group_tag = attribute_group_tag
-        self.attributes = []
-        self.extend(attributes)
-
-    def __getitem__(self, name):
-        """
-        Returns a list of attributes which have name 'name'.
-        """
-        
-        attribute = filter(lambda x: x.name == name, self.attributes)
-        if attribute:
-            return attribute[0]
-        else:
-            raise KeyError("Attribute %r not found" % name)
-
-    def __contains__(self, name):
-        return len(filter(lambda x: x.name == name, self.attributes))
-
-    def __iter__(self):
-        return (a.name for a in self.attributes)
-
-    iterkeys = __iter__
-
-    def __setitem__(self, name, attribute):
-        """
-        Sets an attribute in the attribute group. Note that the key is
-        ignored and the attribute is queried for its name.
-        """
-
-        return self.append(attribute)
-
-    def __delitem__(self, name):
-        self.attributes = filter(lambda x: x.name != name,
-				 self.attributes)
-
-    def append(self, attribute):
-        return self.extend([attribute])
-
-    def extend(self, attributes):
-        """
-        Sets the attributes for the attribute group.
-        """
-
-        for a in attributes:
-            assert isinstance(a, Attribute), \
-                   "attribute must be of type Attribute!"
-
-        for a in attributes:
-            # XXX: Instead of replacing the attribute, do we want to
-            # append the value to the attribute here?
-            del self[a.name]
-            self.attributes.append(a)
-
-    @property
-    def packed_value(self):
-        """
-        Convert the AttributeGroup to binary.
-        """
-
-        # conver the attribute_group_tag to binary
-        tag = struct.pack('>b', self.attribute_group_tag)
-
-        # convert each of the attributes to binary
-        attributes = [a.packed_value for a in self.attributes]
-
-        # concatenate everything and return
-        return tag + ''.join(attributes)
-
-    def __repr__(self):
-        return '<IPPAttributeGroup (%r, %r)>' % (self.attribute_group_tag, self.attributes)
Index: rver/lib/ipp/constants.py
===================================================================
--- server/lib/ipp/constants.py	(revision 1176f8b7b194ba6ef8b67441c731da5624cb71d4)
+++ 	(revision )
@@ -1,314 +1,0 @@
-#!/usr/bin/python
-
-class Misc():
-    """
-    Miscellaneous config options for the IPP server.
-    """
-    
-    IPP_VERSION = "1.1"
-    IPP_PORT = 8000
-
-    def __init__(self): pass
-
-class JobStates():
-    """
-    Job state codes, as defined by RFC 2911, Section 4.3.7
-    """
-    
-    PENDING    = 3
-    HELD       = 4
-    PROCESSING = 5
-    STOPPED    = 6
-    CANCELLED  = 7
-    ABORTED    = 8
-    COMPLETE   = 9
-
-    def __init__(self): pass
-
-class PrinterStates():
-    """
-    Printer state codes, as defined by RFC 2911, Section 4.4.11
-    """
-
-    IDLE       = 3
-    PROCESSING = 4
-    STOPPED    = 5
-
-    def __init__(self): pass
-
-class Operations():
-    """
-    IPP and CUPS IPP Operations, as defined in various RFCs:
-
-        0x0002 - 0x0012      RFC 2911 (Section 4.4.15)
-        0x0013 - 0x0015      RFC 3380 (Section 4)
-        0x0016 - 0x001b      RFC 3995 (Section 7.1)
-        0x0022 - 0x0031      RFC 3998 (Section 14.3)
-        0x4000 - 0x4027      CUPS IPP Actions
-    """
-
-    # These are defined in RFC 2911, Section 4.4.15
-    PRINT_JOB              = 0x0002
-    PRINT_URI              = 0x0003
-    VALIDATE_JOB           = 0x0004
-    CREATE_JOB             = 0x0005
-    SEND_DOCUMENT          = 0x0006
-    SEND_URI               = 0x0007
-    CANCEL_JOB             = 0x0008
-    GET_JOB_ATTRIBUTES     = 0x0009
-    GET_JOBS               = 0x000a
-    GET_PRINTER_ATTRIBUTES = 0x000b
-    #HOLD_JOB              = 0x000c
-    #RELEASE_JOB           = 0x000d
-    #RESTART_JOB           = 0x000e
-    PAUSE_PRINTER          = 0x0010
-    RESUME_PRINTER         = 0x0011
-    #PURGE_JOBS            = 0x0012
-
-    # These are defined in RFC 3380, Section 4
-    SET_PRINTER_ATTRIBUTES           = 0x0013
-    SET_JOB_ATTRIBUTES               = 0x0014
-    #GET_PRINTER_SUPPORTED_VALUES    = 0x0015
-
-    # These are defined in RFC 3995, Section 7.1
-    #CREATE_PRINTER_SUBSCRIPTION     = 0x0016
-    #CREATE_JOB_SUBSCRIPTION         = 0x0017
-    #GET_SUBSCRIPTION_ATTRIBUTES     = 0x0018
-    #GET_SUBSCRIPTIONS               = 0x0019
-    #RENEW_SUBSCRIPTION              = 0x001a
-    #CANCEL_SUBSCRIPTION             = 0x001b
-
-    # These are defined in RFC 3998, Section 14.3
-    #ENABLE_PRINTER                  = 0x0022
-    #DISABLE_PRINTER                 = 0x0023
-    #PAUSE_PRINTER_AFTER_CURRENT_JOB = 0x0024
-    #HOLD_NEW_JOBS                   = 0x0025
-    #RELEASE_HELD_NEW_JOBS           = 0x0026
-    #DEACTIVATE_PRINTER              = 0x0027
-    #ACTIVATE_PRINTER                = 0x0028
-    #RESTART_PRINTER                 = 0x0029
-    #SHUTDOWN_PRINTER                = 0x002a
-    #STARTUP_PRINTER                 = 0x002b
-    #REPROCESS_JOB                   = 0x002c
-    #CANCEL_CURRENT_JOB              = 0x002d
-    #SUSPEND_CURRENT_JOB             = 0x002e
-    #RESUME_JOB                      = 0x002f
-    PROMOTE_JOB                      = 0x0030
-    #SCHEDULE_JOB_AFTER              = 0x0031
-
-    # These are special CUPS actions, defined in:
-    # http://www.cups.org/documentation.php/spec-ipp.html
-    #PRIVATE               = 0x4000
-    CUPS_GET_DEFAULT       = 0x4001
-    CUPS_GET_PRINTERS      = 0x4002
-    #CUPS_ADD_PRINTER      = 0x4003
-    #CUPS_DELETE_PRINTER   = 0x4004
-    CUPS_GET_CLASSES       = 0x4005
-    #CUPS_ADD_CLASS        = 0x4006
-    #CUPS_DELETE_CLASS     = 0x4007
-    #CUPS_ACCEPT_JOBS      = 0x4008
-    #CUPS_REJECT_JOBS      = 0x4009
-    #CUPS_SET_DEFAULT      = 0x400a
-    #CUPS_GET_DEVICES      = 0x400b
-    #CUPS_GET_PPDS         = 0x400c
-    #CUPS_MOVE_JOB         = 0x400d
-    #CUPS_AUTHENTICATE_JOB = 0x400e
-    #CUPS_GET_PPD          = 0x400f
-    CUPS_GET_DOCUMENT      = 0x4027
-
-    def __init__(self): pass
-
-class SuccessCodes():
-    """
-    Success status codes as defined in RFC 2911, Section 13
-    """
-    
-    OK                           = 0x0000
-    OK_SUBST                     = 0x0001
-    OK_CONFLICT                  = 0x0002
-    OK_IGNORED_SUBSCRIPTIONS     = 0x0003
-    OK_IGNORED_NOTIFICATIONS     = 0x0004
-    OK_TOO_MANY_EVENTS           = 0x0005
-    OK_BUT_CANCEL_SUBSCRIPTION   = 0x0006
-
-    def __init__(self): pass
-
-class ClientErrorCodes():
-    """
-    Client error codes as defined in RFC 2911, Section 13
-    """
-    
-    BAD_REQUEST                  = 0x0400
-    FORBIDDEN                    = 0x0401
-    NOT_AUTHENTICATED            = 0x0402
-    NOT_AUTHORIZED               = 0x0403
-    NOT_POSSIBLE                 = 0x0404
-    TIMEOUT                      = 0x0405
-    NOT_FOUND                    = 0x0406
-    GONE                         = 0x0407
-    REQUEST_ENTITY               = 0x0408
-    REQUEST_VALUE                = 0x0409
-    DOCUMENT_FORMAT              = 0x040a
-    ATTRIBUTES                   = 0x040b
-    URI_SCHEME                   = 0x040c
-    CHARSET                      = 0x040d
-    CONFLICT                     = 0x040e
-    COMPRESSION_NOT_SUPPORTED    = 0x040f
-    COMPRESSION_ERROR            = 0x0410
-    DOCUMENT_FORMAT_ERROR        = 0x0411
-    DOCUMENT_ACCESS_ERROR        = 0x0412
-    ATTRIBUTES_NOT_SETTABLE      = 0x0413
-    IGNORED_ALL_SUBSCRIPTIONS    = 0x0414
-    TOO_MANY_SUBSCRIPTIONS       = 0x0415
-    IGNORED_ALL_NOTIFICATIONS    = 0x0416
-    PRINT_SUPPORT_FILE_NOT_FOUND = 0x0417
-
-    def __init__(self): pass
-
-class ServerErrorCodes():
-    """
-    Server error codes as defined in RFC 2911, Section 13
-    """
-
-    INTERNAL_ERROR              = 0x0500
-    OPERATION_NOT_SUPPORTED     = 0x0501
-    SERVICE_UNAVAILABLE         = 0x0502
-    VERSION_NOT_SUPPORTED       = 0x0503
-    DEVICE_ERROR                = 0x0504
-    TEMPORARY_ERROR             = 0x0505
-    NOT_ACCEPTING               = 0x0506
-    PRINTER_BUSY                = 0x0507
-    ERROR_JOB_CANCELLED         = 0x0508
-    MULTIPLE_JOBS_NOT_SUPPORTED = 0x0509
-    PRINTER_IS_DEACTIVATED      = 0x050a
-
-    def __init__(self): pass
-
-class StatusCodes(SuccessCodes, ClientErrorCodes, ServerErrorCodes):
-    pass
-
-class CUPSPrinterType():
-    """
-    Printer types as defined by cups_ptype_e in the CUPS API
-    specification:
-    
-    http://www.cups.org/documentation.php/doc-1.3/api-cups.html#cups_ptype_e
-    """
-
-    LOCAL         = 0x000000
-    CLASS         = 0x000001
-    REMOTE        = 0x000002
-    BW            = 0x000004
-    COLOR         = 0x000008
-
-    DUPLEX        = 0x000010
-    STAPLE        = 0x000020
-    COPIES        = 0x000040
-    COLLATE       = 0x000080
-
-    PUNCH         = 0x000100
-    COVER         = 0x000200
-    BIND          = 0x000400
-    SORT          = 0x000800
-
-    SMALL         = 0x001000
-    MEDIUM        = 0x002000
-    LARGE         = 0x004000
-    VARIABLE      = 0x008000
-
-    IMPLICIT      = 0x010000
-    DEFAULT       = 0x020000
-    FAX           = 0x040000
-    REJECTING     = 0x080000
-
-    DELETE        = 0x100000
-    NOT_SHARED    = 0x200000
-    AUTHENTICATED = 0x400000
-    COMMANDS      = 0x800000
-
-    OPTIONS       = 0x00e6ff
-
-    def __init__(self): pass
-
-class AttributeTags():
-    """
-    Contains constants for the attribute IPP tags, as defined by RFC
-    2565.
-    """
-    
-    ZERO_NAME_LENGTH   = 0x00
-    OPERATION          = 0x01
-    JOB                = 0x02
-    END                = 0x03
-    PRINTER            = 0x04
-    UNSUPPORTED        = 0x05
-    SUBSCRIPTION       = 0x06
-    EVENT_NOTIFICATION = 0x07
-
-    def __init__(self): pass
-
-class OutOfBandTags():
-    """
-    Contains constants for the out-of-band value IPP tags, as defined
-    by RFC 2565.
-    """
-    
-    UNSUPPORTED      = 0x10
-    DEFAULT          = 0x11
-    UNKNOWN          = 0x12
-    NO_VALUE         = 0x13
-    NOT_SETTABLE     = 0x15
-    DELETE_ATTRIBUTE = 0x16
-    ADMIN_DEFINE     = 0x17
-
-    def __init__(self): pass
-
-class IntegerTags():
-    """
-    Contains constants for the integer value IPP tags, as defined by
-    RFC 2565.
-    """
-    
-    GENERIC = 0x20
-    INTEGER = 0x21
-    BOOLEAN = 0x22
-    ENUM    = 0x23
-
-    def __init__(self): pass
-
-class OctetStringTags():
-    """
-    Contains constants for the octetString value IPP tags, as defined
-    by RFC 2565.
-    """
-    
-    UNSPECIFIED_OCTETSTRING = 0x30
-    DATETIME                = 0x31
-    RESOLUTION              = 0x32
-    RANGE_OF_INTEGER        = 0x33
-    BEG_COLLECTION          = 0x34
-    TEXT_WITH_LANGUAGE      = 0x35
-    NAME_WITH_LANGUAGE      = 0x36
-    END_COLLECTION          = 0x37
-
-    def __init__(self): pass
-
-class CharacterStringTags():
-    """
-    Contains constants for the character-string value IPP tags, as
-    defined by RFC 2565.
-    """
-    
-    GENERIC               = 0x40
-    TEXT_WITHOUT_LANGUAGE = 0x41
-    NAME_WITHOUT_LANGUAGE = 0x42
-    KEYWORD               = 0x44
-    URI                   = 0x45
-    URI_SCHEME            = 0x46
-    CHARSET               = 0x47
-    NATURAL_LANGUAGE      = 0x48
-    MIME_MEDIA_TYPE       = 0x49                                    
-    MEMBER_ATTR_NAME      = 0x4a
-
-class Tags(AttributeTags, OutOfBandTags, IntegerTags, OctetStringTags, CharacterStringTags):
-    pass
Index: rver/lib/ipp/request.py
===================================================================
--- server/lib/ipp/request.py	(revision 1b9d629f2f8906a9b86ff315ebcd52dca4d0ffe2)
+++ 	(revision )
@@ -1,240 +1,0 @@
-#!/usr/bin/python
-
-import sys, struct, logging
-from .attributegroup import AttributeGroup
-from .attribute import Attribute
-from .value import Value
-from .constants import AttributeTags
-
-# initialize logger
-logger = logging.getLogger(__name__)
-
-class Request():
-    """
-    From RFC 2565:
-    
-    The encoding for an operation request or response consists of:
-    -----------------------------------------------
-    |                  version-number             |   2 bytes  - required
-    -----------------------------------------------
-    |               operation-id (request)        |
-    |                      or                     |   2 bytes  - required
-    |               status-code (response)        |
-    -----------------------------------------------
-    |                   request-id                |   4 bytes  - required
-    -----------------------------------------------------------
-    |               xxx-attributes-tag            |   1 byte  |
-    -----------------------------------------------           |-0 or more
-    |             xxx-attribute-sequence          |   n bytes |
-    -----------------------------------------------------------
-    |              end-of-attributes-tag          |   1 byte   - required
-    -----------------------------------------------
-    |                     data                    |   q bytes  - optional
-    -----------------------------------------------
-    """
-
-    # either give the version, operation_id, request_id,
-    # attribute_sequence, and data, or a file handler (request) which
-    # can be read from to get the request
-    def __init__(self, version=None, operation_id=None, request_id=None,
-                 attribute_groups=[], data=None, request=None, length=sys.maxint):
-        """
-        Create a Request.  Takes either the segments of the request
-        separately, or a file handle for the request to parse.  If the
-        file handle is passed in, all other arguments are ignored.
-
-        Keyword arguments for passing in the segments of the request:
-        
-            version -- a tuple of two signed chars, identifying the
-                       major version and minor version numbers of the
-                       request
-                            
-            operation_id -- a signed short, identifying the id of the
-                            requested operation
-
-            request_id -- a signed int, identifying the id of the
-                          request itself.
-
-            attribute_groups -- a list of Attributes, at least length 1
-
-            data -- (optional) variable length, containing the actual
-                    data of the request
-
-        Keyword arguments for passing in the raw request:
-
-            request -- a file handle that supports the read()
-                       operation
-        """
-
-        if request is None:
-            # make sure the version number isn't empty
-            assert version is not None
-            # make sure verison is a tuple of length 2
-            assert isinstance(version, tuple)
-            assert len(version) == 2
-            # make sure the operation id isn't empty
-            assert operation_id is not None
-            # make sure the request id isn't empty
-            assert request_id is not None
-            # make sure attribute_groups is a list of Attributes
-            for a in attribute_groups: assert isinstance(a, AttributeGroup)
-            
-        # if the request isn't None, then we'll read directly from
-        # that file handle
-        if request is not None:
-            # read the version-number (two signed chars)
-            self.version        = struct.unpack('>bb', request.read(2))
-            length -= 2
-            logger.debug("version-number : (0x%X, 0x%X)" % self.version)
-
-            # read the operation-id (or status-code, but that's only
-            # for a response) (signed short)
-            self.operation_id   = struct.unpack('>h', request.read(2))[0]
-            length -= 2
-            logger.debug("operation-id : 0x%X" % self.operation_id)
-
-            # read the request-id (signed int)
-            self.request_id     = struct.unpack('>i', request.read(4))[0]
-            length -= 4
-            logger.debug("request-id : 0x%X" % self.request_id)
-
-            # now we have to read in the attributes.  Each attribute
-            # has a tag (1 byte) and a sequence of values (n bytes)
-            self.attribute_groups = []
-
-            # read in the next byte
-            next_byte = struct.unpack('>b', request.read(1))[0]
-            length -=1
-            logger.debug("next byte : 0x%X" % next_byte)
-
-            # as long as the next byte isn't signaling the end of the
-            # attributes, keep looping and parsing attributes
-            while next_byte != AttributeTags.END:
-                
-                attribute_group_tag = next_byte
-                logger.debug("attribute-tag : %i" % attribute_group_tag)
-
-                attributes = []
-
-                next_byte = struct.unpack('>b', request.read(1))[0]
-                length -= 1
-                logger.debug("next byte : 0x%X" % next_byte)
-
-                while next_byte > 0x0F:
-                    
-                    # read in the value tag (signed char)
-                    value_tag     = next_byte
-                    logger.debug("value-tag : 0x%X" % value_tag)
-                    
-                    # read in the length of the name (signed short)
-                    name_length   = struct.unpack('>h', request.read(2))[0]
-                    length -= 2
-                    logger.debug("name-length : %i" % name_length)
-                    
-                    if name_length != AttributeTags.ZERO_NAME_LENGTH:
-                        # read the name (a string of name_length bytes)
-                        name          = request.read(name_length)
-                        length -= name_length
-                        logger.debug("name : %s" % name)
-                    
-                        # read in the length of the value (signed short)
-                        value_length  = struct.unpack('>h', request.read(2))[0]
-                        length -= 2
-                        logger.debug("value-length : %i" % value_length)
-                    
-                        # read in the value (string of value_length bytes)
-                        value         = request.read(value_length)
-                        length -= value_length
-                        
-                        ippvalue = Value.unpack(value_tag, value)
-                        logger.debug("value : %s" % ippvalue.value)
-
-                        # create a new Attribute from the data we just
-                        # read in, and add it to our attributes list
-                        attributes.append(Attribute(name, [ippvalue]))
-
-                    else:
-                        # read in the length of the value (signed short)
-                        value_length  = struct.unpack('>h', request.read(2))[0]
-                        length -= 2
-                        logger.debug("value-length : %i" % value_length)
-                    
-                        # read in the value (string of value_length bytes)
-                        value         = request.read(value_length)
-                        length -= value_length
-
-                        ippvalue = Value.unpack(value_tag, value)
-                        logger.debug("value : %s" % ippvalue.value)
-
-                        # add another value to the last attribute
-                        attributes[-1].values.append(ippvalue)
-
-                    # read another byte
-                    next_byte = struct.unpack('>b', request.read(1))[0]
-                    length -= 1
-
-                self.attribute_groups.append(AttributeGroup(
-                    attribute_group_tag, attributes))
-
-            # once we hit the end-of-attributes tag, the only thing
-            # left is the data, so go ahead and read all of it
-            assert length >= 0
-            self.data = request.read(length)
-            logger.debug("data : %s" % self.data)
-
-        # otherwise, just set the class variables to the keyword
-        # arguments passed in
-        else:
-            self.version = (version[0], version[1])
-            self.operation_id = operation_id
-            self.request_id = request_id
-            self.attribute_groups = attribute_groups
-            self.data = data
-
-    def getAttributeGroup(self, tag):
-        return filter(lambda x: x.attribute_group_tag == tag,
-                      self.attribute_groups)
-
-    @property
-    def packed_value(self):
-        """
-        Packs the value data into binary data.
-        """
-
-        # make sure the version number isn't empty
-        assert self.version is not None
-        # make sure verison is a tuple of length 2
-        assert isinstance(self.version, tuple)
-        assert len(self.version) == 2
-        # make sure the operation id isn't empty
-        assert self.operation_id is not None
-        # make sure the request id isn't empty
-        assert self.request_id is not None
-        # make sure attribute_groups is a list of Attributes
-        assert len(self.attribute_groups) > 0
-        for a in self.attribute_groups: assert isinstance(a, AttributeGroup)
-
-        # convert the version, operation id, and request id to binary
-        preattributes = struct.pack('>bbhi',
-                                    self.version[0],
-                                    self.version[1],
-                                    self.operation_id,
-                                    self.request_id)
-
-        # convert the attribute groups to binary
-        attribute_groups = ''.join([a.packed_value for a in self.attribute_groups])
-
-        # conver the end-of-attributes-tag to binary
-        end_of_attributes_tag = struct.pack('>b', AttributeTags.END)
-
-        # convert the data to binary
-        if self.data is not None:
-            data = ''.join([struct.pack('>b', x) for x in self.data])
-        else:
-            data = ''
-
-        # append everything together and return it
-        return preattributes + attribute_groups + end_of_attributes_tag + data
-
-    def __repr__(self):
-        return '<IPPRequest (version=%r, operation_id=%x, request_id=%r, attribute_groups=%r)>' % (self.version, self.operation_id, self.request_id, self.attribute_groups)
Index: rver/lib/ipp/value.py
===================================================================
--- server/lib/ipp/value.py	(revision 556a285856700d2cda14bac66c4b8040d198d073)
+++ 	(revision )
@@ -1,302 +1,0 @@
-#!/usr/bin/python
-
-import sys, struct, logging
-from .constants import OutOfBandTags, IntegerTags, OctetStringTags, CharacterStringTags
-
-# initialize logger
-logger = logging.getLogger(__name__)
-
-def setter(prop):
-    def f(func):
-        return property(prop.fget, func, prop.fdel, prop.__doc__)
-    return f
-
-class Value(object):
-    """
-    An IPP value consists of a tag and a value.
-
-    From RFC 2565:
-     -----------------------------------------------
-     |                   value-tag                 |   1 byte
-     -----------------------------------------------
-     |            name-length  (value is 0x0000)   |   2 bytes
-     -----------------------------------------------
-     |              value-length  (value is v)     |   2 bytes
-     -----------------------------------------------
-     |                     value                   |   v bytes
-     -----------------------------------------------    
-    """
-
-    def __init__(self, value_tag=None, value=None):
-        """
-        Initialize a Value.  There are three different ways you can
-        call this method:
-
-            Value() -- creates an empty Value instance
-
-            Value(value_tag, value) -- creates a Value instance from
-            a non-binary value
-
-        If you create an empty Value instance, once you have set
-        value_tag and value, you can retrieve the packed value from
-        the packed_value property.
-
-        Arguments:
-
-            value_tag -- one byte, identifying the type of value
-
-            value -- variable size, containing the actual value.
-            It should be a string or number.
-        """
-
-        # make sure the arguments are valid
-        if value is not None:
-            assert value_tag is not None, \
-                   "value_tag must not be null because " + \
-                   "value is not null!"
-
-        # initialize member variables
-        self.value_tag = value_tag # one byte, the type of value
-        self.value     = value     # non-binary value of self.value
-
-    @classmethod
-    def unpack(cls, value_tag, packed_value):
-        """Unpack a binary IPP value
-
-        Unpacks a binary string into a Value object.
-
-        """
-        return cls(value_tag, cls._unpack(value_tag, packed_value))
-
-    @staticmethod
-    def _unpack(value_tag, packed_value):
-        """
-        Given self.value_tag and self.packed_value, unpack the binary
-        value into either a string or number.  These values MUST NOT
-        be null.
-
-        Returns: unpacked value
-
-        """
-
-        assert value_tag is not None, \
-               "Cannot unpack values with unspecified value tag!"
-        assert packed_value is not None, \
-               "Cannot unpack null values!"
-
-        value = None
-
-        # out-of-band value tags
-        if value_tag == OutOfBandTags.UNSUPPORTED or \
-               value_tag == OutOfBandTags.DEFAULT or \
-               value_tag == OutOfBandTags.UNKNOWN or \
-               value_tag == OutOfBandTags.NO_VALUE:
-            value_size = 0
-            value = ''
-
-        # integer value tags
-        elif value_tag == IntegerTags.INTEGER:
-            value = struct.unpack('>i', packed_value)[0]
-        elif value_tag == IntegerTags.BOOLEAN:
-            value = struct.unpack('>b', packed_value)[0]
-        elif value_tag == IntegerTags.ENUM:
-            value = struct.unpack('>i', packed_value)[0]
-
-        
-        elif value_tag == OctetStringTags.DATETIME:
-            # field  octets  contents                  range
-            # -----  ------  --------                  -----
-            #   1      1-2   year                      0..65536
-            #   2       3    month                     1..12
-            #   3       4    day                       1..31
-            #   4       5    hour                      0..23
-            #   5       6    minutes                   0..59
-            #   6       7    seconds                   0..60
-            #                (use 60 for leap-second)
-            #   7       8    deci-seconds              0..9
-            #   8       9    direction from UTC        '+' / '-'
-            #   9      10    hours from UTC            0..11
-            #  10      11    minutes from UTC          0..59
-
-            value = struct.unpack('>hbbbbbbcbb', packed_value)
-            
-        elif value_tag == OctetStringTags.RESOLUTION:
-            # OCTET-STRING consisting of nine octets of 2
-            # SIGNED-INTEGERs followed by a SIGNED-BYTE. The first
-            # SIGNED-INTEGER contains the value of cross feed
-            # direction resolution. The second SIGNED-INTEGER contains
-            # the value of feed direction resolution. The SIGNED-BYTE
-            # contains the units
-
-            value = struct.unpack('>iib', packed_value)
-            
-        elif value_tag == OctetStringTags.RANGE_OF_INTEGER:
-            # Eight octets consisting of 2 SIGNED-INTEGERs.  The first
-            # SIGNED-INTEGER contains the lower bound and the second
-            # SIGNED-INTEGER contains the upper bound.
-
-            value = struct.unpack('>ii', packed_value)
-
-        elif value_tag == OctetStringTags.TEXT_WITH_LANGUAGE or \
-                 value_tag == OctetStringTags.NAME_WITH_LANGUAGE:
-            a = struct.unpack('>h', packed_value[:2])[0]
-            b = struct.unpack('>%ss' % a, packed_value[2:a+2])[0]
-            c = struct.unpack('>h', packed_value[a+2:a+4])[0]
-            d = struct.unpack('>%ss' % c, packed_value[a+4:][0])
-            value = (a, b, c, d)
-
-        # character string value tags
-        elif value_tag == \
-                 CharacterStringTags.TEXT_WITHOUT_LANGUAGE or \
-                 value_tag == \
-                 CharacterStringTags.NAME_WITHOUT_LANGUAGE:
-            value = str(packed_value)
-        elif value_tag == CharacterStringTags.GENERIC or \
-                 value_tag == CharacterStringTags.KEYWORD or \
-                 value_tag == CharacterStringTags.URI or \
-                 value_tag == CharacterStringTags.URI_SCHEME or \
-                 value_tag == CharacterStringTags.CHARSET or \
-                 value_tag == CharacterStringTags.NATURAL_LANGUAGE or \
-                 value_tag == CharacterStringTags.MIME_MEDIA_TYPE:
-            value = str(packed_value)
-
-        # anything else that we didn't handle
-        else:
-            if value is None:
-                value = packed_value
-
-        return value
-
-    @property
-    def packed_value(self):
-        """
-        Given self.value_tag and self.value, pack the value into
-        binary form.  These values MUST NOT be null.
-
-        Returns: packed_value
-
-        """
-        
-        assert self.value_tag is not None, \
-               "cannot pack value with null value tag!"
-        assert self.value is not None, \
-               "cannot pack null value!"
-
-        packed_value = None
-
-        # out-of-band value tags
-        if self.value_tag == OutOfBandTags.UNSUPPORTED or \
-               self.value_tag == OutOfBandTags.DEFAULT or \
-               self.value_tag == OutOfBandTags.UNKNOWN or \
-               self.value_tag == OutOfBandTags.NO_VALUE:
-            packed_value = ''
-
-        # integer value tags
-        elif self.value_tag == IntegerTags.INTEGER:
-            packed_value = struct.pack('>i', self.value)
-        elif self.value_tag == IntegerTags.BOOLEAN:
-            packed_value = struct.pack('>b', self.value)
-        elif self.value_tag == IntegerTags.ENUM:
-            packed_value = struct.pack('>i', self.value)
-
-        # octet string value tags
-        elif self.value_tag == OctetStringTags.DATETIME:
-            # field  octets  contents                  range
-            # -----  ------  --------                  -----
-            #   1      1-2   year                      0..65536
-            #   2       3    month                     1..12
-            #   3       4    day                       1..31
-            #   4       5    hour                      0..23
-            #   5       6    minutes                   0..59
-            #   6       7    seconds                   0..60
-            #                (use 60 for leap-second)
-            #   7       8    deci-seconds              0..9
-            #   8       9    direction from UTC        '+' / '-'
-            #   9      10    hours from UTC            0..11
-            #  10      11    minutes from UTC          0..59
-
-            packed_value = struct.pack('>hbbbbbbcbb', *self.value)
-            
-        elif self.value_tag == OctetStringTags.RESOLUTION:
-            # OCTET-STRING consisting of nine octets of 2
-            # SIGNED-INTEGERs followed by a SIGNED-BYTE. The first
-            # SIGNED-INTEGER contains the value of cross feed
-            # direction resolution. The second SIGNED-INTEGER contains
-            # the value of feed direction resolution. The SIGNED-BYTE
-            # contains the units
-
-            packed_value = truct.pack('>iib', *self.value)
-            
-        elif self.value_tag == OctetStringTags.RANGE_OF_INTEGER:
-            # Eight octets consisting of 2 SIGNED-INTEGERs.  The first
-            # SIGNED-INTEGER contains the lower bound and the second
-            # SIGNED-INTEGER contains the upper bound.
-
-            packed_value = struct.pack('>ii', *self.value)
-
-        elif self.value_tag == OctetStringTags.TEXT_WITH_LANGUAGE or \
-                 self.value_tag == OctetStringTags.NAME_WITH_LANGUAGE:
-            
-            a_bin = struct.pack('>h', self.value[0])
-            b_bin = struct.pack('>%ss' % self.value[0], self.value[1])
-            c_bin = struct.pack('>h', self.value[2])
-            d_bin = struct.pack('>%ss' % self.value[2], self.value[3])
-
-            packed_value = a_bin + b_bin + c_bin + d_bin
-
-        # character string value tags
-        elif self.value_tag == \
-                 CharacterStringTags.TEXT_WITHOUT_LANGUAGE or \
-                 self.value_tag == \
-                 CharacterStringTags.NAME_WITHOUT_LANGUAGE:
-
-            packed_value = struct.pack('>%ss' % len(self.value),
-                                       self.value)
-                    
-        elif self.value_tag == CharacterStringTags.GENERIC or \
-                 self.value_tag == CharacterStringTags.KEYWORD or \
-                 self.value_tag == CharacterStringTags.URI or \
-                 self.value_tag == CharacterStringTags.URI_SCHEME or \
-                 self.value_tag == CharacterStringTags.CHARSET or \
-                 self.value_tag == CharacterStringTags.NATURAL_LANGUAGE or \
-                 self.value_tag == CharacterStringTags.MIME_MEDIA_TYPE:
-            
-            packed_value = struct.pack('>%ss' % len(self.value),
-                                       self.value)
-
-        else:
-            packed_value = self.value
-
-        return packed_value
-
-    @setter(packed_value)
-    def packed_value(self, packed_value):
-        """Replace a value using a new packed value
-
-        Unpacks a new packed_value (of the same value_tag).
-
-        """
-        self.value = self._unpack(self.value_tag, packed_value)
-
-    @property
-    def packed_value_size(self):
-        """
-        Get the size of the value in bytes.
-        """
-        
-        return len(self.packed_value)
-
-    @property
-    def total_size(self):
-        """
-        Get the total size of the IPP value.
-        """
-
-        # 1 byte for the tag
-        return self.packed_value_size + 1
-
-    def __str__(self):
-        return str(self.value)
-
-    def __repr__(self):
-        return '<IPPValue (%x, %r)>' % (self.value_tag, self.value)
Index: rver/lib/server/__init__.py
===================================================================
--- server/lib/server/__init__.py	(revision 776a659edf6a8f91d1a6fb6f009e4abac41bf064)
+++ 	(revision )
@@ -1,314 +1,0 @@
-#!/usr/bin/python
-
-import logging, BaseHTTPServer
-import time
-import ipp
-import ipp.constants as const
-from ipp.constants import Operations
-
-logging.basicConfig(level=logging.DEBUG)
-
-def handler_for(operation):
-    def f(func):
-        func.ipp_operation = operation
-        return func
-    return f
-
-class IPPRequestHandler(object):
-    def _ipp_dispatch(self, request):
-        for d in dir(self):
-            if getattr(getattr(self, d), "ipp_operation", None) == request.operation_id:
-                return getattr(self, d)
-        return self.unknown_operation
-
-    def unknown_operation(self, request, response):
-        print "Received UNKNOWN OPERATION %x" % request.operation_id
-        response.operation_id = const.StatusCodes.OPERATION_NOT_SUPPORTED
-
-class PrinterRequestHandler(IPPRequestHandler):
-    def __init__(self, name):
-        self.name = name
-
-    @handler_for(Operations.GET_PRINTER_ATTRIBUTES)
-    def get_printer_attributes(self, request, response):
-        printer_attributes = ipp.AttributeGroup(const.AttributeTags.PRINTER)
-        printer_attributes[:] = \
-            ipp.Attribute("printer-uri-supported",
-                          [ipp.Value(ipp.Tags.URI,
-                                     "ipp://localhost:8000/printers/"+self.name)]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("uri-authentication-supported",
-                          [ipp.Value(ipp.Tags.KEYWORD,
-                                     "none")]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("uri-security-supported",
-                          [ipp.Value(ipp.Tags.KEYWORD,
-                                     "none")]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("printer-name",
-                          [ipp.Value(ipp.Tags.NAME_WITHOUT_LANGUAGE,
-                                     self.name)]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("printer-state",
-                          [ipp.Value(ipp.Tags.ENUM,
-                                     const.PrinterStates.IDLE)]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("printer-state-reasons",
-                          [ipp.Value(ipp.Tags.KEYWORD,
-                                     "none")]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("ipp-versions-supported",
-                          [ipp.Value(ipp.Tags.KEYWORD,
-                                     "1.0"),
-                           ipp.Value(ipp.Tags.KEYWORD,
-                                     "1.1")]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("operations-supported",
-                          [ipp.Value(ipp.Tags.ENUM,
-                                     Operations.GET_JOBS)]
-                          ) #XXX: We should query ourself for the supported operations
-        printer_attributes[:] = \
-            ipp.Attribute("charset-configured",
-                          [ipp.Value(ipp.Tags.CHARSET,
-                                     "utf-8")]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("charset-supported",
-                          [ipp.Value(ipp.Tags.CHARSET,
-                                     "utf-8")]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("natural-language-configured",
-                          [ipp.Value(ipp.Tags.NATURAL_LANGUAGE,
-                                     "en-us")]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("generated-natural-language-supported",
-                          [ipp.Value(ipp.Tags.NATURAL_LANGUAGE,
-                                     "en-us")]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("document-format-default",
-                          [ipp.Value(ipp.Tags.MIME_MEDIA_TYPE,
-                                     "application/octet-stream")]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("document-format-supported",
-                          [ipp.Value(ipp.Tags.MIME_MEDIA_TYPE,
-                                     "application/octet-stream"),
-                           ipp.Value(ipp.Tags.MIME_MEDIA_TYPE,
-                                     "audio/mp3")]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("printer-is-accepting-jobs",
-                          [ipp.Value(ipp.Tags.BOOLEAN,
-                                     True)]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("queued-job-count",
-                          [ipp.Value(ipp.Tags.INTEGER,
-                                     1)]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("pdl-override-supported",
-                          [ipp.Value(ipp.Tags.KEYWORD,
-                                     "not-attempted")]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("printer-up-time",
-                          [ipp.Value(ipp.Tags.INTEGER,
-                                     int(time.time()))]
-                          )
-        printer_attributes[:] = \
-            ipp.Attribute("compression-supported",
-                          [ipp.Value(ipp.Tags.KEYWORD,
-                                     "none")]
-                          )
-
-        response.attribute_groups.append(printer_attributes)
-        response.operation_id = const.StatusCodes.OK
-        print "get_printer_attributes called"
-
-class RootRequestHandler(IPPRequestHandler):
-    printers = [PrinterRequestHandler(name="sipbmp3")]
-
-    @handler_for(Operations.CUPS_GET_DEFAULT)
-    def cups_get_default(self, request, response):
-        print "get_default called"
-        return self.printers[0].get_printer_attributes(request, response)
-
-    @handler_for(Operations.CUPS_GET_PRINTERS)
-    def cups_get_printers(self, request, response):
-        print "get_printers called"
-        response.operation_id = const.StatusCodes.OK
-        for p in self.printers:
-            # Each printer will append a new printer attribute group.
-            p.get_printer_attributes(request, response)
-
-    @handler_for(Operations.CUPS_GET_CLASSES)
-    def cups_get_classes(self, request, response):
-        print "get_classes called"
-        response.operation_id = const.StatusCodes.OK
-        # We have no printer classes, so nothing to return.
-
-class GutenbachIPPHandler(BaseHTTPServer.BaseHTTPRequestHandler):
-    def setup(self):
-        self.root = RootRequestHandler()
-        BaseHTTPServer.BaseHTTPRequestHandler.setup(self)
-
-    def handle_one_request(self):
-        self.raw_requestline = self.rfile.readline()
-        if not self.raw_requestline:
-            self.close_connection = 1
-            return
-        if not self.parse_request(): # An error code has been sent, just exit
-            return
-        self.handle_ipp()
-
-    def handle_ipp(self):
-        length = int(self.headers.getheader('content-length', 0))
-        request = ipp.Request(request=self.rfile,
-                              length=length)
-        print "Received request:", repr(request)
-
-        response_kwargs = {}
-        response_kwargs['version'] = request.version
-        response_kwargs['operation_id'] = const.StatusCodes.INTERNAL_ERROR
-        response_kwargs['request_id'] = request.request_id
-        response_kwargs['attribute_groups'] = [
-            ipp.AttributeGroup(const.AttributeTags.OPERATION,
-                               [ipp.Attribute('attributes-charset',
-                                              [ipp.Value(ipp.Tags.CHARSET,
-                                                         'utf-8'
-                                                         )]),
-                                ipp.Attribute('attributes-natural-language',
-                                              [ipp.Value(ipp.Tags.NATURAL_LANGUAGE,
-                                                         'en-us'
-                                                         )])
-                                ])]
-
-        response = ipp.Request(**response_kwargs)
-        handler = self.root._ipp_dispatch(request)
-        handler(request, response)
-        print "Sending response:", repr(response)
-
-        self.send_response(200, "o hai")
-        self.send_header("Content-Type", "application/ipp")
-        self.send_header("Connection", "close")
-        self.end_headers()
-        self.wfile.write(response.packed_value)
-
-    def get_jobs(self, request, response_kwargs):
-        """get-jobs response"""
-
-        job_attributes = [ipp.Attribute('job-id',
-                                       [ipp.Value(ipp.Tags.INTEGER,
-                                                 12345,
-                                                 )]),
-                          ipp.Attribute('job-name',
-                                       [ipp.Value(ipp.Tags.NAME_WITHOUT_LANGUAGE,
-                                                 'foo',
-                                                 )]),
-                          ipp.Attribute('job-originating-user-name',
-                                       [ipp.Value(ipp.Tags.NAME_WITHOUT_LANGUAGE,
-                                                 'jhamrick',
-                                                 )]),
-                          ipp.Attribute('job-k-octets',
-                                       [ipp.Value(ipp.Tags.INTEGER,
-                                                 1,
-                                                 )]),
-                          ipp.Attribute('job-state',
-                                       [ipp.Value(ipp.Tags.ENUM,
-                                                 const.JobStates.HELD,
-                                                 )]),
-                          ipp.Attribute('job-printer-uri',
-                                       [ipp.Value(ipp.Tags.URI,
-                                                 'ipp://localhost:8000/printers/foo',
-                                                 )])]
-
-
-        #req_op_attributes = request.getAttributeGroup(ipp.Tags.OPERATION_ATTRIBUTES_TAG)
-        #print req_op_attributes
-        #printer_uri = req_op_attributes[0].getAttribute('printer-uri')
-
-        op_attributes = [ipp.Attribute('attributes-charset',
-                                      [ipp.Value(ipp.Tags.CHARSET,
-                                                'utf-8',
-                                                )]),
-                         ipp.Attribute('attributes-natural-language',
-                                      [ipp.Value(ipp.Tags.NATURAL_LANGUAGE,
-                                                'en-us',
-                                                )])]
-        
-        job_attribute_group = ipp.AttributeGroup(const.AttributeTags.JOB,
-                                                 job_attributes)
-        op_attributes_group = ipp.AttributeGroup(const.AttributeTags.OPERATION,
-                                                 op_attributes)
-        response_kwargs['attribute_groups'] = [op_attributes_group,job_attribute_group]
-        response_kwargs['operation_id'] = const.StatusCodes.OK
-
-        return response_kwargs
-
-    ##### Printer Commands
-
-    def print_job(self, request):
-        pass
-
-    def validate_job(self, request):
-        pass
-
-    #def get_jobs(self, request):
-    #    pass
-
-    def print_uri(self, request):
-        pass
-
-    def create_job(self, request):
-        pass
-
-    def pause_printer(self, request):
-        pass
-
-    def resume_printer(self, request):
-        pass
-
-    def set_printer_attributes(self, request):
-        pass
-
-    ##### Job Commands
-
-    def cancel_job(self, request):
-        pass
-
-    def get_job_attributes(self, request):
-        pass
-
-    def send_document(self, request):
-        pass
-
-    def send_uri(self, request):
-        pass
-
-    def set_job_attributes(self, request):
-        pass
-
-    def cups_get_document(self, request):
-        pass
-
-    def restart_job(self, request):
-        pass
-
-    def promote_job(self, request):
-        pass
-
-if __name__ == '__main__':
-    server_address = ('', 8000)
-    httpd = BaseHTTPServer.HTTPServer(server_address, GutenbachIPPHandler)
-    httpd.serve_forever()
Index: rver/lib/server/exceptions.py
===================================================================
--- server/lib/server/exceptions.py	(revision 776a659edf6a8f91d1a6fb6f009e4abac41bf064)
+++ 	(revision )
@@ -1,13 +1,0 @@
-class InvalidJobException(Exception):
-    def __init__(self, jobid):
-	self.jobid = jobid
-
-    def __str__(self):
-	return "Job with id '%d' does not exist!" % self.jobid
-
-class InvalidPrinterStateException(Exception):
-    def __init__(self, message):
-	self.message = message
-
-    def __str__(self):
-	return self.message
Index: rver/lib/server/job.py
===================================================================
--- server/lib/server/job.py	(revision 776a659edf6a8f91d1a6fb6f009e4abac41bf064)
+++ 	(revision )
@@ -1,81 +1,0 @@
-import os
-from exceptions import InvalidJobException, \
-     InvalidPrinterStateException
-
-class Job(object):
-
-    def __init__(self, document=None):
-	"""Initialize a Gutenbach job.
-
-	This sets the status to 'initializing' and optionally sets the
-	document to print to the value of document.
-	"""
-	 
-	self._jobid = None
-	self._status = 'initializing'
-	self._document = document
-	self._printer = None
-
-    @property
-    def jobid(self):
-	return self._jobid
-
-    @jobid.setter
-    def jobid(self, val):
-	raise AttributeError("Setting jobid is illegal!")
-
-    @property
-    def document(self):
-	return self._document
-
-    @document.setter
-    def document(self, path):
-	if not os.path.exists(path):
-	    raise IOError("Document '%s' does not exist!" % path)
-	self._document = path
-
-    @property
-    def status(self):
-	return self._status
-
-    @status.setter
-    def status(self, val):
-	raise AttributeError(
-	    "Setting status directly is illegal!  " + \
-	    "Please use enqueue(), play(), or finish().")
-
-    @property
-    def printer(self):
-	return self._printer
-
-    @printer.setter
-    def printer(self, val):
-	raise AttributeError(
-	    "Setting printer directly is illegal!  " + \
-	    "Please use enqueue().")
-
-    def enqueue(self, printer):
-	if self.status != 'initializing':
-	    raise InvalidJobException(
-		"Cannot enqueue a job that has " + \
-		"already been initialized!")
-	self._printer = printer
-	self._status = 'active'
-
-    def play(self):
-	if self.status != 'active':
-	    raise InvalidJobException(
-		"Cannot play an inactive job!")
-	
-	self._status = 'playing'
-	# TODO: add external call to music player
-	self.printer.complete_job(self.jobid)
-
-    def finished(self):
-	self._status = 'finished'
-
-    def __repr__(self):
-	return str(self)
-
-    def __str__(self):
-	return "<Job %d>" % self.jobid
Index: rver/lib/server/printer.py
===================================================================
--- server/lib/server/printer.py	(revision 776a659edf6a8f91d1a6fb6f009e4abac41bf064)
+++ 	(revision )
@@ -1,80 +1,0 @@
-import ipp
-import alsaaudio as aa
-from ipp.attribute import Attribute
-from .exceptions import InvalidJobException, \
-     InvalidPrinterStateException
-
-class Printer(object):
-
-    def __init__(self, name, card, mixer):
-
-	self.name = name
-
-	if card >= len(aa.cards()):
-	    raise aa.ALSAAudioError(
-		"Audio card at index %d does not exist!" % card)
-	elif mixer not in aa.mixers(card):
-	    raise aa.ALSAAudioError(
-		"Audio mixer '%s' does not exist!" % mixer)
-	
-	self.card = card
-	self.mixer = mixer
-
-	self.finished_jobs = []
-	self.active_jobs = []
-	self.jobs = {}
-
-	self._next_jobid = -1
-
-    @property
-    def next_jobid(self):
-	self._next_jobid += 1
-	return self._next_jobid
-
-    @next_jobid.setter
-    def next_jobid(self, val):
-	raise AttributeError("Setting next_jobid is illegal!")
-
-    def print_job(self, job):
-	jobid = self.next_jobid
-	self.active_jobs.append(jobid)
-	self.jobs[jobid] = job
-	job.jobid = jobid
-	job.enqueue(self)
-	return jobid
-
-    def complete_job(self, jobid):
-	job = self.active_jobs.pop(0)
-	if job.jobid != jobid:
-	    raise InvalidJobException(
-		"Completed job %d has unexpected job id %d!" % \
-		(job.jobid, jobid))
-	
-	self.finished_jobs.append(job)
-	job.finish()
-	return job.jobid
-
-    def start_job(self, jobid):
-	job = self.active_jobs[0]
-	if job.jobid != jobid:
-	    raise InvalidJobException(
-		"Completed job %d has unexpected job id %d!" % \
-		(job.jobid, jobid))
-
-	if job.status == 'playing':
-	    raise InvalidPrinterStateException(
-		"Next job in queue (id %d) is " + \
-		"already playing!" % jobid)
-
-	job.play()
-
-    def get_job(self, jobid):
-	if jobid not in self.jobs:
-	    raise InvalidJobException(jobid)
-	return self.jobs[jobid]
-
-    def __repr__(self):
-	return str(self)
-
-    def __str__(self):
-	return "<Printer '%s'>" % self.name
