Sunday, July 20, 2014

Using Netflix in XBMC with Ubuntu 14.04

I use XBMC to run my home theater system.  It's a great piece of software that can be configured to look and act in a huge variety of ways while remaining straightforward to use and set up.  One of its great features is an add-on system with a rich set of third-party multimedia tools.

Until recently, it was pretty straightforward to have a nice experience in XBMC under Linux using a combination of XBMCFlix and Pipelight.  Pipelight provides a way to run the Silverlight video streaming libraries under Linux and XBMCFlix launches the Netflix player in a full-screen Chrome window.  Installing these add-ons meant that Netflix worked well[1] and integrated right into XBMC.

Recently, however, Google has dropped support for the Netscape Plugin API (NPAPI) which Pipelight relies on to make Silverlight work.  Since XBMCFlix launches Chrome and can't be configured to use Firefox, this means that upgrading to a recent version of Chrome breaks Netflix support in XBMC on Linux.

Fortunately it's easy to work around this and trick the Chrome launcher used by XBMC to use Firefox instead.  It's a dirty hack and isn't a great solution for a general purpose desktop (especially as it involves uninstalling Chrome, which you might want to keep) but it does the trick on my entertainment system.

Here's how to do it:

  1. Uninstall Chrome or Chromium.  (On Ubuntu or other Debian-based distribution, you can do this in the Software Center or by typing "sudo apt-get remove google-chrome" (or "sudo apt-get remove chromium-browser" in a terminal.)
  2. Launch Firefox, put it in full-screen mode by pressing F11, and exit it with Alt-F4.  This will make it default to full-screen next time it's launched.
  3. Using your favorite text editor, create a script in your home directory called "google-chrome".  (It's important to make it all lower case and include the hyphen.  It needs to be named exactly "google-chrome", nothing else.)
  4. Copy and paste the following into the text file:

    #!/bin/bash

    for i in $@; do
        if [[ $i =~ ^http ]]
        then
          urls="$urls $i"
        fi
    done

    firefox $urls
  5. Save the file.
  6. Make your script executable so it can be launched by XBMC by opening a terminal and typing "chmod a+x google-chrome".
  7. Put your script where the launcher for Chrome would normally live by typing "sudo mv google-chrome /usr/bin".
Once this is done, you can install Pipelight and XBMCFlix in XBMC as you normally would.  When XBMCFlix goes to launch Chrome, it will launch Firefox instead.

Again, this is a bit of an ugly hack, but if you have a PC that does nothing but run XBMC this will get you up and working quickly.  There are ways that a script like this could be exploited, but the odds of Netflix including one in a URL to target Linux users of XBMC are pretty low, and if you have a single-purpose home theater system there's probably not much damage that could be done anyway.

Now go enjoy some movies!


1: Well, after you lie to Netflix about what operating system you're running. It's 2014 and they are still doing browser user-agent sniffing as if it were useful or effective.

Monday, July 7, 2014

Hiring? Vet your application process! And please don't use Taleo.

I've written a bit before on this topic from my experiences in trying to hire people.  But now I want to say something from the point of view of a job seeker.

Your application system is part of your culture


When hiring, remember that the job listings on your website are an indication of your values and culture.  If your application process is hostile and bureaucratic, applicants will assume working there will be soul-crushing and awful.

In some areas, you can get away with this.  The job market is tight in many sectors right now.  But in others, companies are desperate for talent but are turning it away at the gate.[1]  If you make it difficult for the people you want to hire to apply, they will go somewhere else!  It's like opening a store and putting barbed wire across the door; why would I go somewhere that I'm obviously not wanted?

When designing your application process, walk through it and ask yourself "would I apply for a job here with this process?".

If you're looking to hire people, please don't use Taleo.


(Warning: this section is a bit ranty.  But it's true and a great example of how bad things can be when nobody pays attention to the applicant side of a process.)

Taleo is a "talent management" system, meaning that it's supposed to handle all the easily automatable things around posting a position and tracking applicants.  I don't know how well it performs these functions from the HR/business side, but I have experienced it from the job seeker side several times.  And that experience is universally awful.

Let me give a real-world example of what a job seeker sees when they use your Taleo application system.  First of all, they get absolute state-of-the-1990s web design.  Ugly "hold CONTROL (or the clover key on a Mac)" instructions in tiny boxes that require painful scrolling and careful clicking.  One mistake and the 12 selections you've made in the 4-line high box with 60 options goes poof!

Maybe they want to search instead.  Searching is a thing that people like, right?  Especially if you treat each word as its own search term, so "sales engineer" gets anything with the word "sales" or the word "engineer" anywhere in the posting.  How convenient!

If they've made it this far though, your job seeker has a long (and unsortable) list of possible matches.  But they can't open each match in a new tab or window to compare them, as the links get built with Javascript.  Even better, the real URL to the job is never written to the URL bar in the browser, so you can't open the job in the same window and copy the URL to a new one.  One job at a time is all you get, and when you go back to the list the page jumps to the top, losing your place.  (This one in particular got me when I intended to apply to a large company.  I opened about 10 promising titles from a list of several hundred in new tabs, only to find they were all a generic error page once I was done paging through the available jobs.  Guess where I didn't end up applying to any jobs?[2])

There's much more; Taleo asks that a resume be uploaded but then forces the applicant to put the same data into multiple small ugly boxes, forces a slow process with seemingly infinite page loads, and eventually acts as a black hole where applicants can't check their status or follow up on their applications in any meaningful way.  In short, whether intentionally or not, it's a system designed to prevent people from applying for jobs and to leave them feeling dispirited and angry once they've done so.

Fixing things


What's the ultimate goal of a hiring process?  It's not to build reports on demographics or to create a database of skills and salary requirements for HR; it's to get people into your company.

When you design your hiring process, design the applicant side first!  Think about how someone will search for a job, how they will apply, and how they will follow up afterwards.  You can collect the data HR needs to track while still ensuring that the process is pleasant from the applicant's side.  It's not difficult, but it does require forethought and focus.

And please, don't use Taleo.



1: An example is for basically anyone with technical knowledge of OpenStack. At the last OpenStack Developer Summit in Atlanta, nearly every company there was desperate to hire anyone with OpenStack experience. (See, for example, the last section of the wrapup from day 2.) Companies had HR representatives at their booths on the trade show floor just to negotiate with prospective hires on the spot! It was cutthroat, but these same companies are making it painful to apply to those same jobs now.

2: The big company I was applying to is one of the folks desperate to hire OpenStack talent. I was applying for an OpenStack-related job...

Sunday, March 30, 2014

Adding hardware support to MAAS

MAAS and power

MAAS, or Metal as a Service, is a tool for treating physical servers similarly to cloud resources.  It lets you take a pile of hardware and assign workloads to it without worrying about all the support infrastructure underneath.  In effect, you can plug a bunch of systems into a network and have a Hadoop or OpenStack cluster up, running, and properly configured inside of half an hour.  It's really cool stuff.

But MAAS is targeted at enterprise-level hardware.  It expects things like IPMI to be available to control the hardware at a basic level.  (If your tool is going to be installing an operating system onto a computer, it needs to be able to control that computer at a level below the OS.)  I have a stack of older desktops sitting around gathering dust that I wanted to use as a cluster for experimenting at home.  They still function quite well, but being desktops they don't have enterprise features like IPMI.

Fortunately most desktop BIOSes have a setting to control what they do after a power outage.  Crucially, they can be set to turn the system on when electrical power is restored, regardless of whether the system was on or off when power was lost.  This means I could use an externally controlled power switch in place of IPMI to allow MAAS to power my desktops on and off.

Power on after power failure.  (Helpfully hidden under a random submenu in the BIOS rather than under the 'POWER SETTINGS' heading.)


I used the Web Power Switch 7 from Digital Loggers for this project.  (Warning: almost every page on their site has autoplaying audio and/or video.  Ugh.)  I'll be completely honest here -- price was the reason I chose this switch.  It allows control of up to 8 outlets and cost me just under $140 from Amazon.

My shiny new power switch.  (What do you mean, your datacenter doesn't have an oriental rug?)

Power templates


There are a few bits of MAAS that I had to touch to fully enable support for my DLI switch.  The first and most obvious was creating a power control template to tell MAAS how to talk to the switch.

When MAAS is installed, the templates that tell it how to talk to power management systems are put in /etc/maas/templates/power.  These are shell scripts with a bit of Python evaluation and substitution (surrounded by {{double braces}})that MAAS uses at runtime to build a command to control a specific system.  There are several examples to use there already, and these can be modified to suit your environment.  For example, the SeaMicro SM15000 hardware template looks like this:

# -*- mode: shell-script -*-
#
# Control a system via ipmipower, sending the seamicro specific hex codes
#
{{py: power_mode = 1 if power_change == 'on' else 6 }}

{{ipmitool}} -I lanplus \
    -H {{power_address}} -U {{power_user}} \
    -P {{power_pass}} raw 0x2E 1 0x00 0x7d 0xab \
    {{power_mode}} 0 {{system_id}}

I started by simply replacing one of these templates with my own code.  Because the DLI Web Power Switch can be controlled via HTTP, I just needed to build a GET request for the outlet I wanted to control and send it to the switch, like this:

#!/bin/sh
# -*- mode: shell-script -*-
#
# Control a system via the Digital Loggers, Inc. RESTful interface
#
{{py: power_mode = 'ON' if power_change == 'on' else 'OFF' }}

wget --auth-no-challenge -O /dev/null \
    http://{{power_user}}:{{power_pass}}@{{power_address}}/outlet?{{system_id}}={{power_mode}}

Once I had this template replacing the original sm15k template, I was able to control my switch by telling MAAS I had SeaMicro hardware.  Success!

Better integration


I could have stopped there with a working system.  But it bothered me a bit to lie to MAAS about my hardware.  And what if someday I or someone else wanted to mix the DLI switch with actual SeaMicro hardware?  Replacing the sm15k template with my DLI template wouldn't work then.

It turns out that there are two places in the MAAS code that need to be modified to make it aware of a new power switch.  In the MAAS source, these are src/maasserver/power_parameters.py and src/provisioningserver/enum.pypower_parameters.py is where the real definition of a new power switch is; enum.py just needs an entry in two lists to make the UI show the switch as an option.  (There's a comment in enum.py to the effect that adding the switch name in it will not be necessary at some point in the future, but at least as of March 2014 it still is.)

The code needed to add a power switch is straightforward.  Provide the variables the switch will need to operate a system (e.g. outlet number, IP address of the switch, credentials, etc.) and give them names that can be referenced in the power template later.

Success!


The code I added can be seen in Launchpad (click "expand all" to see the diffs).

Once I added the code there, I was able to rebuild MAAS and see my new switch as an option.  It all works, and now my old desktops have new life!

Monday, January 21, 2013

Integrating your web application with Ubuntu Unity

Starting with version 12.04, Ubuntu has been adding some nifty features that let developers integrate web applications with the desktop.  This makes web applications work similarly to native applications -- icons on the Launcher, nice volume control, desktop notifications, messaging system integration, the whole works.  It's really cool, but not used as widely as I'd like.  So I put together a little sample application to show how easy it is to do!

The API is easy to use, and anyone who is writing a web app should be able to use it.  Basically you just need to make a few simple Javascript calls and your web app will integrate nicely with Ubuntu.  I did come across a few 'gotchas' on the way though, so I'll point those out as well.

Javascript callbacks


A bit of background information is in order here.  Javascript functions are themselves just another type of javascript object, meaning that you can do things like set them as the value of a variable or pass them to a function just like any other piece of data.  There's a good article about this here but you don't need to know all the details to integrate your web app with Unity.  Really all you need to understand is that some of the bits of the integration expect you to pass them a function object, not just a string of code.  (Try to pass in code where a callback is expected and you'll just end up executing that code when the page loads... probably not what you want to happen!)

An example callback:


Unity.Launcher.addAction("Messages",
function(){ unityRedirect("messages");
);


The second parameter passed to addAction() here (which I've highlighted in yellow) is itself a function, making it a callback for addAction().

Basically all you need to remember is that if you want to call code to make something happen in your web app, you need to pass a function in rather than just the code you want to execute.

Getting set up and adding a launcher icon

Before you can do anything fancy, you need to have some sort of way of getting information from the browser to Unity.  Fortunately, there's code in Ubuntu to do this already.  All you need to do is call it:

var Unity = external.getUnityObject(1.0);

This will set up the Unity integration object.  It expects to be told what version of the API to use.  Currently the latest (and only) version is 1.0, so that's what your code should use too.

Now that you have a way to talk to Unity, you need to tell it a bit about your app.  That's done through the Unity.init() method:


Unity.init({
name: "Example Web App",
iconUrl: "http://www.example.com/icon.png",
onInit: unityReady
});


The parameters to init() are pretty straightforward:
  • name - the name of your application
  • iconUrl - a URL pointing to an icon for your application (which should be of reasonable size; don't use your favicon as it will be scaled up and look really ugly!)
  • onInit - a function to call when everything is loaded and ready

Here's one of those gotchas I mentioned earlier: it's really important to keep everything you need to tell Unity to do initially inside of the function you set as your onInit!  The Unity.init() method runs asynchronously -- just because execution has passed to the next line in your code doesn't mean that the Unity object is ready to use!  So don't try something like:


Unity.init({
...
});
Unity.Launcher.addAction(...); // No!  Don't do this!


Doing this is almost certainly going to fail and cause you lots of frustration.  Always do your integration setup inside of your onInit function.

Once you've set up the Unity object, your application will automatically get an icon in the Dash (and on the Launcher when it's running or pinned there).

Our little web app is growing up!  It's just like a desktop application now.

Going beyond the basics: adding functionality Launcher icon

Now that you have an icon in the Launcher, you can make it useful to the user  by adding quicklinks.  These are a menu in your icon that give the user a fast way to jump to different parts of your web application.

Remember the function you passed as the onInit parameter to init() earlier?  This is where it comes in handy.


function unityReady() {
Unity.Launcher.addAction("Messages",
function(){ unityRedirect("messages"); } 
);
}


This will add a quicklink to a page on your site called "messages".

Another gotcha: unfortunately, the quicklink functionality for web applications appears to be broken right now in Ubuntu 12.10.  Even the default applications that are integrated with Unity by default don't work.  Hopefully this will be fixed in the near future!

There's more you can do with the Launcher icon too.  For example, you might want to show the user that they have some unread messages on your site by adding a count to the icon, like this:

Unity.Launcher.setCount(4);

I have 4, um, examples

You can also set a progress bar with Launcher.setProgress().  There's more information about that in the API reference.

Getting more advanced: indicator integration

There's another way of showing how many messages a user has that helps them access their messages faster and is more consistent with the rest of the Unity desktop: the Messaging Indicator.  Fortunately it's easy to integrate with it as well!


Unity.MessagingIndicator.showIndicator(
"Inbox", {
count: 3, 
callback: function(){ unityRedirect("messages"); }
});


The above code will add a new entry to the Messaging Indicator with your application's icon and the title "Inbox".  When the user clicks on it, the callback you specify will be called.  (You probably want to direct the user to the appropriate place on your web application to view or reply to these messages here.)

I should check my messages more often


Finally, why not let the user know when they have received a new message?  You can do that with a desktop notification:


Unity.Notification.showNotification(
"Example Application", // message header
"This is your notification!", // message body
false);


This gives a standard desktop notification to the user with your application's icon.



Another gotcha: The API documentation says that the third parameter to showNotification() (which would be a URL pointing to a custom icon) is optional, but it's really not.  If you don't put something there, your notification won't be shown.  I've used false in my example, which seems to work well.

Now you're ready to get started on your own application!  There's more functionality available that I didn't go over here (such as Media Indicator integration for web apps with multimedia functionality and some optional features of Unity and Launcher integration), so I'd recommend checking out the API documentation as well.  Finally, my example code is available as a complete unit with tons of comments on github.

Go get integrating and have fun!

Tuesday, August 7, 2012

Making the most of job fairs

Last week I complained a bit about my experiences at an employment meetup, and a few days later ended up in a similar situation.  But this one went better, and I've been thinking about why.  I have some suggestions for both organizers of events as well as attendees.

I attended the Boston Startup School demo day for their class of 2012.  (As an aside here, the Boston Startup School is a really cool idea that seems to be doing great things!  If you're in the area and interested in entrepreneurship, I definitely recommend checking them out.)

Anyway, this event was like the meetup I attended in that it had the goal of connecting people looking for jobs with people looking for employees.  But I get the feeling that here everyone on the 'looking' side will end up with a job within a month at the most.  I've been rolling that around in my head for a few days and I think there are a few reasons this worked better.

One reason is that Boston Startup School had quality students and had obviously taught them a ton.  There was nobody there that I felt would be a bad hire for a startup.  Less selective events don't have this advantage, but I think there are still some things to learn and apply anywhere.


For organizers: structure!


The good

The demo day event was structured as a sort of reverse career fair.  All of the graduates did a one-minute pitch to the crowd and then got off the stage.  (And I do mean one minute!  This is the sort of thing that can drag on very easily, and to Boston Startup School's credit they kept it tight.)

After that, everyone mingled and chatted.  The prospective employers were given a book with the names and faces of all the presenters and some space to take notes.  That was extremely helpful to me, as I was able to track down the people I'd written good things about during their pitch and talk to them further.

Having a program planned out ahead of time that let people know who was there and when to expect to start to mingle and chat was key to this.  The companies soaked up the pitches and went straight for the people they wanted to talk to.  I ended up having several great talks and scheduled followup discussions right then and there.


Room for improvement: structure for unstructured discussion

Here's the thing -- unstructured discussion is great.  Having people mingle and talk is going to get things to happen... eventually.  But you can make unstructured discussion easier by structuring the space itself.

I wasn't able to easily identify the people I wanted to talk with in the crowd.  I had to wander around aimlessly, picture book in hand, until I found someone I recognized.  Then I couldn't let them out of my sight until I got the chance to talk to them -- I'd never find them again otherwise.

The job-seekers were grouped into a small number of 'tracks' during their pitches; why not have areas of the room for those tracks to congregate in?  Or maybe give each of the job-seekers a distinctive article of clothing to wear.  If they each had a bright pink hat, I'd at least be able to narrow down the number of faces I was comparing to my notes.


For participants: be identifiable!

The people I was able to find and speak with quickly were easily identifiable.  And I was surprised by how easy this was to do (and embarrassed that I hadn't thought of some of these things myself!).


Branding

The simplest but most effective thing I saw was having a personalized name tag.  Everyone had a black and white sticker on their chest with their name on it, but some people had made their own with brightly colored marker.  Having "JEFF" in bright red and blue on your name tag makes you stand out across the room in a way that "Jeff Smith Jr., Weyland-Yutani Corporation" in Times New Roman doesn't.

If nobody else has a name tag, even better!  You'll stand out more.  Don't worry about looking silly; at least people are looking at you.


Have a 'hook'

One of the graduates had the best technique for getting conversations started that I have ever seen.  He used maybe 20 of his 60 seconds to tell a brief story about his life, but left out the ending!  He described being on a train in Sibera, unable to speak Russian, and having a halting conversation with a man who gave him the best advice of his life.  He ended his presentation by saying that if anyone wanted to know what that advice was to seek him out afterward.

It's cheesy.  Definitely.  But at the very least it's something to open a conversation with.  ("What did that man say to you, anyway?")  This guy was totally mobbed the entire time the discussion period was going on.


Be practiced

As I said in my previous post, the only way to get better at talking to strangers is to practice.  Find a way to get out of your comfort zone and keep at it.  It's hard -- very hard, sometimes -- but it's a skill that can be learned.


Get to it!

I'm glad I had the opportunity to compare these events.  Having them so close together was a lucky break for me.  I hope this helps people make the most of job fairs on both the planning and attending sides... I know I'll be doing things differently in the future myself.

Monday, July 30, 2012

Lots of IT jobs in Boston (or "How not to network")

I attended a meetup last week for IT professionals, recruiters, and job-seekers in the Boston area.  For me it was a combination of wanting to start networking a bit more actively and maybe seeing if there were any PHP programmers who might fit in at Libboo.

The good and the bad...

My first observation was that there were far more people looking to hire than there were people looking for jobs.  This is good news if you're out looking!  At least this week in Boston.  (I'll have to wait to see if this is a trend or just coincidence.)

But my second observation was that most of the people there really didn't know how to make the most of an event like this.  (This applied both to the job-seekers as well as the prospective employers, which surprised me a bit -- I think of recruiters or managers as being generally more outgoing.)

It turned out that the organizer of the event was running late and couldn't get there to start things off.  So most people were sitting or standing around by themselves, sort of staring at the walls and waiting for someone to come along and connect them with what they wanted.

Don't do that.

Don't be that person.  If you wait around for someone else, you aren't in control of what you get... if you get anything at all.

I know it's difficult to start talking to someone.  Believe me, I really do.  I'm the biggest introvert that I know, and have been all my life.  I still would rather stay home or hang out with a very small group of good friends than go out to a noisy club.  I get exhausted dealing with people, even when I'm having a good time.

But if you're like me, you've heard this advice before.  Again, I know.  I heard it all the time too.  "Just go start talking to people!"  Or maybe, "They're just as nervous as you, you know."  None of those things helped, and frankly I suspect there are a lot of people who really like meeting new people and feel energized afterwards.  Having people lie to me or make the problem sound trivial always hurt more than it helped.

So what does help?

I can't speak for everyone.  (Even if I could, I wouldn't want to.  That's way too many new people to meet.)  But what worked for me was putting myself in a situation where I was forced to be social and outgoing.  I got a job as a Sales Engineer.  (That's someone sitting more or less between sales and engineering; participating in the sales process and meetings but keeping more to the technical aspects of a sale.)

And you know what?  It was hard.  Really, really hard.  I was uncomfortable and stressed and unhappy.  But it got easier, and the more I did it the easier it got.  I still don't jump out of bed every morning wanting to go out and talk to a hundred strangers, but now I'm comfortable enough doing it that I was able to get people talking at a meetup.

I don't know that I'd recommend changing jobs to everyone, but do something that pushes you past your comfort zone.  It will be hard at first, but sticking with it makes it easier.  Join a club, take a class, see a professional if you have to, but forcing yourself to learn and improve these skills will compensate you far more than the discomfort it causes.  (And these things are learnable!  Some people seem to be born knowing how to make small talk and put others at ease.  But the rest of us can learn with enough effort.)

Friday, July 20, 2012

Altering ebooks -- adding pages to an existing epub


I've been working with ebooks (specifically epub files) lately, particularly with modifying them.  There are some great tools out there to help with this work.

Calibre is fantastic at converting between formats, and better yet it has a command-line interface, so it can be part of an automated script.  It can also do some simple editing of metadata, allowing you to update the author, title, etc. of a book.  But there's no functionality for editing the content of an ebook.

Sigil is another awesome tool.  This is the go-to program for editing the content of an ebook.  It has one major drawback for me though -- it's not scriptable.  There's no way to do something like adding an informational page into an existing ebook without doing it by hand.

I did some research but wasn't able to really find anything that would let me add a page to an ebook non-interactively, so I did it myself.  I thought this might be useful to other people looking to modify epub files, I've included it below.

A couple of notes to keep in mind:

  • I wrote this in PHP because I needed to interface with a large existing PHP codebase.  This would be even easier to do in Python, but the logic here is pretty straightforward and should be easily adapted.
  • The epub format is really simple.  At heart it's some XML, some HTML (or XHTML), maybe a few images, all wrapped up in a zip container.  That's fortunate in that there are a lot of libraries out there to work on exactly these formats.
  • That said, there are some tricky bits to how the epub zip has to be structured.  You can't just throw everything into a zip and rename it, which means the built-in PHP zip libraries don't work for it.
The code is here, and a quick text overview follows after.

<?php
    /*** Helper function from php.net ***/
    // This allows the delete of a directory and its contents
    function rrmdir($dir)
    { 
        if (is_dir($dir))
        { 
            $objects = scandir($dir); 
            foreach ($objects as $object)
            { 
                if ($object != "." && $object != "..")
                     if (filetype($dir."/".$object) == "dir")
                         rrmdir($dir."/".$object);
                     else
                         unlink($dir."/".$object);  
            } 
        reset($objects); 
        rmdir($dir); 
       }
     }


    /*** Setup ***/
    // Since this is an example, we can hard-code some things...
    $loc = '/home/fader/Projects/Libboo/epub-test'; // Where epubs live
    $epub = 'test.epub'; // The epub we will modify
    $newepub = 'new.epub'; // The new epub we will generate
    $added_page = 'newpage.xhtml'; // The new page we're going to insert into it


    // Allocate a directory to work in
    $temp_path = sys_get_temp_dir() . "/" . uniqid("epub-");
    mkdir($temp_path) or die("Couldn't create temporary path.");


    /*** Let's do this thing! ***/
    // Open the epub archive
    $zip = new ZipArchive;
    $res = $zip->open($epub);
    if ($res !== TRUE)
        die("Couldn't open epub as a zip.");


    // Unzip the epub into a temporary location
    $zip->extractTo($temp_path);


    // *** Dig into the ebook container
    // The path is defined by the epub spec, so as long as this is a compliant
    // epub file, we should be able to find fit at this location
    $container_path = $temp_path . "/META-INF/container.xml";
    $container_xml = file_get_contents($container_path);
    if ($container_xml === FALSE)
        die("Couldn't open container XML file.");


    // Look in the container to find the spine
    $container = new SimpleXMLElement($container_xml);
    $spine_path = $temp_path . "/" . $container->rootfiles[0]->rootfile["full-path"];


    // Pull up the spine
    $spine_xml = file_get_contents($spine_path);
    if ($spine_xml === FALSE)
        die("Couldn't open the table of contents.");


    // Copy the new page into the correct location
    if (!copy($added_page, dirname($spine_path) . "/" . basename($added_page)))
        die("Unable to copy new page into temporary location.");


    // *** Decide where to insert a node
    // For this example, we'll just plug it in as the third element
    // Unfortunately, SimpleXML is too... simple to let us insert a node into
    // an arbitrary position, so we use the DOM object
    $dom = new DOMDocument;
    $dom->loadXML($spine_xml);
    // Fortunately the structure for an epub spine is pretty simple.  So we can
    // just get the list of pages ("item"s) and run down the tree a bit.
    $items = $dom->getElementsByTagName("item");
    $itemrefs = $dom->getElementsByTagName("itemref");


    // Let'ss grab the third element
    // (NB: Pretty much any epub should have at least 3 items.
    // (ncx, css, title, pages...)  But boundary checks are always a Good Thing.)
    if ($items->length < 3)
        die("Book is ridiculously short.");


    // *** Create and insert the new nodes
    // We'll need two nodes here -- one for the "item" and one for the "itemref".
    // Both need to be present for the new page to be found by the reader.
    $newitem = $dom->createElement("item");
    $newitem->setAttribute("id", "newpageid0");
    $newitem->setAttribute("href", basename($added_page));
    $newitem->setAttribute("media-type", "application/xhtml+xml");
    $insert_point_item = $items->item(3);
    $insert_point_item->parentNode->insertBefore($newitem, $insert_point_item);


    $newitemref = $dom->createElement("itemref");
    $newitemref->setAttribute("idref", "newpageid0");
    $newitemref->setAttribute("linear", "yes");
    $insert_point_itemref = $itemrefs->item(3);
    $insert_point_itemref->parentNode->insertBefore($newitemref, $insert_point_itemref);


    // *** Write it out
    $newxml = $dom->saveXML();
    $result = file_put_contents($spine_path, $newxml);
    if ($result === FALSE)
        die("Unable to write new XML file.");


    // *** Zip everything back up again
    // The mimetype needs to be stored, not compressed.  Unfortunately I have not
    // seen a way to do this with the PHP ZipArchive object.
    // This is the quick, dirty, nonportable, ugly way to do it:
    system("zip -q0Xj $temp_path/$newepub " . $temp_path . "/mimetype");
    // Since we're already calling the system zip binary, this is about 30 lines smaller
    // than using the PHP zip object to accomplish the same thing:
    system("cd $temp_path ; zip -q0Xj $newepub mimetype ; zip -qXr $newepub * -x mimetype");


    /*** Clean up after ourselves ***/
    // Move the new epub file to the working directory
    if (!rename($temp_path . "/" . $newepub, $loc . "/" . $newepub))
        die("Unable to move new epub file to $loc.");
    // Delete the temporary path
    rrmdir($temp_path);
?>

In short, here's what the above does:

  • Sets up a convenience function for cleaning up later
  • Extracts the contents of the epub file into a temporary location
  • Reads the container XML file (specified by the epub spec) to find the index of files (which could be in an arbitrary location inside the epub)
  • Copies in the new page to be added
  • Creates two XML nodes
    • One is the location of the file containing the new page
    • The other is a referent indicating where in the book that page should fall
  • Adds these nodes to the index
  • Zips everything back up
  • Moves the new epub to a specified location
  • Cleans up the temporary files created
It's pretty straightforward, all told.  The tricky bit is in zipping the files up -- epub requires that the mimetype file (specifying that it is an epub) must be the first file in the archive and stored rather than compressed.  This bit's tricky in PHP, so I copped out and just called the native system binary.

If anyone has any questions I'm happy to discuss this... it's a fun toy problem!