diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,31 @@
+Copyright Dino Morelli 2010
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Dino Morelli nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/README b/README
new file mode 100644
--- /dev/null
+++ b/README
@@ -0,0 +1,107 @@
+-----
+Building:
+
+   Easy with cabal-install, of course:
+
+      $ cabal install epub-metadata
+
+   Or the conventional way:
+
+      $ runhaskell Setup.hs configure
+      $ runhaskell Setup.hs build
+      $ runhaskell Setup.hs test
+      $ runhaskell Setup.hs haddock
+      $ runhaskell Setup.hs install
+
+
+-----
+Why was this done?
+
+   The motivation for this project grew out of my desire to take charge
+   of missing or incorrect ePub metadata in books I have purchased. I
+   started out using the Calibre open source tools for examining this
+   info. Limitations and incomplete implementation of those tools led
+   me here to build a more complete implementation in the programming
+   language that I love beyond all others.
+
+
+-----
+Why didn't I just use existing solutions?
+
+   - Calibre ebook-meta utility
+
+      I experienced various problems using this software, such as:
+
+      Incomplete and in some cases incorrect handling of tags that can
+      exist more than once, particularly when they are differentiated
+      using attributes according to the spec.
+
+      Unable to display many fields in the OPF Package Document metadata
+      specification. Unable to manipulate data that is represented as
+      attributes of tags in the OPF spec.
+
+      Astonishingly slow performance. The command-line tool in this
+      new Haskell project is more than 45 times faster at parsing
+      and displaying ePub metadata. I'm going to blame Python here for
+      Calibre's performance. This has had a big impact on projects where
+      I've been processing hundreds of ePubs in batch operations.
+
+      To be fair, an effort is being made in Calibre to work with both
+      ePub and Sony LRF book documents. That is going to naturally require
+      a lowest-common-denominator approach. My focus here was to work
+      with ePub only, and thoroughly support the OPF specification.
+
+
+   - epub on Hackage, EPUB E-Book construction support library
+
+      The focus of this project seems to be with building new documents,
+      not parsing existing files. And there is a specific attempt to
+      do more than the metadata, to gather up the content and other
+      metafiles that make up an ePub for creation.
+
+      Examining Codec.Ebook.OPF.Types, most of the metadata fields
+      from the OPF Package Document spec are missing or aren't modeled
+      thoroughly. I felt to contribute to this project, I would have
+      had to significantly rip up the types and redesign them.
+
+      At this time I felt it was a better solution for me to start fresh
+      with modelling these types and code to manipulate them. That said,
+      I would be very interested in combining the epub and epub-metadata
+      projects at some point in some way that makes sense.
+
+
+   - zip-archive on Hackage
+
+      I started out using zip-archive for this work but soon found that
+      it has problems with some poorly-made ePub files. Nearly 300 or 40%
+      of the books I have are unreadable at this time with zip-archive.
+
+      As ugly as it sounds, I found the most reliable solution right
+      now is to use the unzip shell utility to extract the relevant XML
+      documents from ePub books. This is why the dependency on HSH. I'm
+      not thrilled with this situation and would like to find time to
+      dig into the zip file spec and submit patches to zip-archive.
+
+      But also note that epub-meta is obscenely fast even with invoking
+      a shell for unzipping. Haskell FTW!
+
+
+-----
+A word about the version numbering scheme:
+
+   4-part: major.minor.status.build
+   3-part: major.status.build
+
+   status:
+      0 alpha
+      1 beta
+      2 release candidate
+      3 release
+
+   examples:
+      1.3.0.2         v1.3 alpha build 2
+      1.2.1.0         v1.2 beta build 0
+      4.2.24          v4 release candidate build 24
+      2.10.3.5        v2.10 release build 5 (say they were bug fixes)
+      1.5.2.20090818  Can even use a date for build
+                      v1.5 release candidate 2009-08-18 build
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,39 @@
+#! /usr/bin/env runhaskell
+
+-- Copyright: 2010 Dino Morelli
+-- License: BSD3 (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+import Control.Monad ( unless )
+import Distribution.Simple
+import System.Cmd ( system )
+import System.Directory ( createDirectoryIfMissing )
+import System.FilePath
+import System.Posix.Files ( createSymbolicLink, fileExist )
+
+
+main = defaultMainWithHooks (simpleUserHooks 
+   { postBuild = customPostBuild
+   , runTests = testRunner
+   } )
+
+   where
+      -- Create symlink to the binary after build for developer 
+      -- convenience
+      customPostBuild _ _ _ _ = do
+         let binDir = "bin"
+         let binName = "epub-meta"
+         let destPath = binDir </> binName
+
+         createDirectoryIfMissing True binDir
+
+         linkExists <- fileExist destPath
+         unless linkExists $ do
+            createSymbolicLink
+               (".." </> "dist" </> "build" </> binName </> binName)
+               destPath
+
+      -- Target for running all unit tests
+      testRunner _ _ _ _ = do
+         system $ "runhaskell -isrc testsuite/runtests.hs"
+         return ()
diff --git a/TODO b/TODO
new file mode 100644
--- /dev/null
+++ b/TODO
@@ -0,0 +1,1 @@
+- Better errors on parse failure, this will emerge along with more unit testing
diff --git a/epub-metadata.cabal b/epub-metadata.cabal
new file mode 100644
--- /dev/null
+++ b/epub-metadata.cabal
@@ -0,0 +1,36 @@
+name:                epub-metadata
+version:             1.0.2
+cabal-version:       >= 1.2
+build-type:          Simple
+license:             BSD3
+license-file:        LICENSE
+copyright:           2010 Dino Morelli
+author:              Dino Morelli
+maintainer:          Dino Morelli <dino@ui3.info>
+stability:           experimental
+homepage:            http://ui3.info/d/proj/epub-metadata.html
+synopsis:            Library and utility for parsing and manipulating 
+                     ePub metadata
+description:         Library for parsing and manipulating ePub OPF 
+                     metadata. An attempt has been made here to very 
+                     thoroughly implement the metadata portion of the OPF 
+                     Package Document specification. Also included is a 
+                     command-line utility to dump ePub metadata to stdout 
+                     in a human-readable form.
+category:            Codec, Text
+tested-with:         GHC>=6.12.1
+
+library
+   exposed-modules:  Codec.Epub.IO,
+                     Codec.Epub.Opf.Metadata,
+                     Codec.Epub.Opf.Metadata.Format,
+                     Codec.Epub.Opf.Metadata.Parse
+   hs-source-dirs:   src
+   build-depends:    base >= 3 && < 5, HSH, hxt, mtl
+   ghc-options:      -Wall
+
+executable           epub-meta
+   main-is:          Codec/Epub/Opf/epub-meta.hs
+   hs-source-dirs:   src
+   build-depends:    base >= 3 && < 5
+   ghc-options:      -Wall
diff --git a/src/Codec/Epub/IO.hs b/src/Codec/Epub/IO.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Epub/IO.hs
@@ -0,0 +1,57 @@
+-- Copyright: 2010 Dino Morelli
+-- License: BSD3 (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+{-# LANGUAGE FlexibleContexts #-}
+
+{- | Functions for doing some disk IO with ePub documents
+
+   Note that these functions do their work by using the external 
+   unzip utility.
+-}
+module Codec.Epub.IO
+   ( extractFileFromZip, opfPath )
+   where
+
+import Control.Monad.Error
+import HSH.Command
+import Text.Printf
+import Text.XML.HXT.Arrow
+
+
+{- | Extract a file from a zipfile.
+   This is here because ePub files are really just zip files.
+-}
+extractFileFromZip :: (MonadIO m, MonadError [Char] m)
+   => FilePath    -- ^ path to zip file
+   -> FilePath    -- ^ path within zip file to extract
+   -> m String    -- ^ contents of expected file
+extractFileFromZip zipPath filePath = do
+   let dearchiver = "unzip"
+   result <- liftIO $ tryEC $ run
+      ((printf "%s -p %s %s" dearchiver zipPath filePath) :: String)
+   case result of
+      Left ps -> throwError $
+         printf "[ERROR %s  zip file: %s  path in zip: %s  status: %s]"
+            dearchiver zipPath filePath (show ps)
+      Right output -> return output
+
+
+-- | Get the path within an ePub file to the OPF Package Document
+opfPath :: (MonadError String m, MonadIO m)
+   => FilePath    -- ^ path to ePub zip file
+   -> m String    -- ^ path within ePub to the OPF Package Document
+opfPath zipPath = do
+   containerContents <- extractFileFromZip zipPath
+      "META-INF/container.xml"
+
+   result <- liftIO $ runX (
+      readString [(a_validate, v_0)] containerContents
+      >>> deep (isElem >>> hasName "rootfile")
+      >>> getAttrValue "full-path"
+      )
+
+   case result of
+      (p : []) -> return p
+      _        -> throwError
+         "ERROR: rootfile full-path missing from META-INF/container.xml"
diff --git a/src/Codec/Epub/Opf/Metadata.hs b/src/Codec/Epub/Opf/Metadata.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Epub/Opf/Metadata.hs
@@ -0,0 +1,92 @@
+-- Copyright: 2010 Dino Morelli
+-- License: BSD3 (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+{- | Data types for working with the metadata of ePub documents
+
+   These data types were constructed by studying the IDPF OPF 
+   specification for ePub documents found here:
+
+   <http://www.idpf.org/2007/opf/OPF_2.0_final_spec.html>
+-}
+module Codec.Epub.Opf.Metadata
+   ( OPFPackage (..)
+   , EMTitle (..)
+   , EMCreator (..)
+   , EMDate (..)
+   , EMId (..)
+   , EpubMeta (..)
+   , emptyEpubMeta
+   )
+   where
+
+
+{- | opf:package tag
+
+   Note that we are not yet storing the data that comes after
+   \/package\/metadata in an OPF Package Document. But that may
+   be added at a later time.
+-}
+data OPFPackage = OPFPackage
+   { opVersion :: String  -- ^ version attr
+   , opUniqueId :: String  -- ^ unique-identifier attr
+   , opMeta :: EpubMeta  -- ^ metadata child element contents
+   }
+   deriving (Eq, Show)
+
+
+-- | dc:title tag, xml:lang attr, content
+data EMTitle = EMTitle (Maybe String) String
+   deriving (Eq, Show)
+
+-- | dc:creator tag, opf:role attr, opf:file-as attr, content
+data EMCreator = EMCreator (Maybe String) (Maybe String) String
+   deriving (Eq, Show)
+
+-- | dc:date tag, opf:event attr, content
+data EMDate = EMDate (Maybe String) String
+   deriving (Eq, Show)
+
+-- | dc:identifier tag, id attr, opf:scheme attr, content
+data EMId = EMId String (Maybe String) String
+   deriving (Eq, Show)
+
+-- | opf:metadata tag
+data EpubMeta = EpubMeta
+   { emTitles :: [EMTitle]   -- ^ at least one required
+   , emCreators :: [EMCreator]
+   , emContributors :: [EMCreator]
+   , emSubjects :: [String]  -- ^ dc:subject tags
+   , emDescription :: Maybe String  -- ^ dc:description tags
+   , emPublisher :: Maybe String  -- ^ dc:publisher tag
+   , emDates :: [EMDate]
+   , emType :: Maybe String  -- ^ dc:type tag
+   , emFormat :: Maybe String  -- ^ dc:format tag
+   , emIds :: [EMId]          -- ^ at least one required
+   , emSource :: Maybe String  -- ^ dc:source tag
+   , emLangs :: [String]    -- ^ dc:language tags, at least one required
+   , emRelation :: Maybe String  -- ^ dc:relation tag
+   , emCoverage :: Maybe String  -- ^ dc:coverage tag
+   , emRights :: Maybe String  -- ^ dc:rights tag
+   }
+   deriving (Eq, Show)
+
+-- | Note: This isn't valid as-is, some required values are empty lists!
+emptyEpubMeta :: EpubMeta
+emptyEpubMeta = EpubMeta
+   { emTitles = []   -- one required
+   , emCreators = []
+   , emContributors = []
+   , emSubjects = []
+   , emDescription = Nothing
+   , emPublisher = Nothing
+   , emDates = []
+   , emType = Nothing
+   , emFormat = Nothing
+   , emIds = []       -- one required
+   , emSource = Nothing
+   , emLangs = []     -- one required
+   , emRelation = Nothing
+   , emCoverage = Nothing
+   , emRights = Nothing
+   }
diff --git a/src/Codec/Epub/Opf/Metadata/Format.hs b/src/Codec/Epub/Opf/Metadata/Format.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Epub/Opf/Metadata/Format.hs
@@ -0,0 +1,131 @@
+-- Copyright: 2010 Dino Morelli
+-- License: BSD3 (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+-- | Module for pretty-printing ePub metadata info
+module Codec.Epub.Opf.Metadata.Format
+   ( opfToString
+   )
+   where
+
+import Text.Printf
+
+import Codec.Epub.Opf.Metadata
+
+
+formatSubline :: String -> Maybe String -> String
+formatSubline _   Nothing = ""
+formatSubline key (Just value) = printf "   %s: %s\n" key value
+
+
+packageToString :: (String, String) -> String
+packageToString (version, uniqueId) =
+   "package\n" ++
+   (formatSubline "version" (Just version)) ++
+   (formatSubline "unique-identifier" (Just uniqueId))
+
+
+titleToString :: EMTitle -> String
+titleToString (EMTitle Nothing title) = printf "title: %s\n" title
+titleToString (EMTitle lang title) =
+   "title\n" ++
+   (formatSubline "lang" lang) ++
+   (formatSubline "title" (Just title))
+
+
+creatorToString :: EMCreator -> String
+creatorToString (EMCreator Nothing Nothing creator) =
+   printf "creator: %s\n" creator
+creatorToString (EMCreator role fileAs creator) =
+   "creator\n" ++
+   (formatSubline "role" role) ++
+   (formatSubline "file-as" fileAs) ++
+   (formatSubline "creator" (Just creator))
+
+
+contributorToString :: EMCreator -> String
+contributorToString (EMCreator Nothing Nothing contributor) =
+   printf "contributor: %s\n" contributor
+contributorToString (EMCreator role fileAs contributor) =
+   "contributor\n" ++
+   (formatSubline "role" role) ++
+   (formatSubline "file-as" fileAs) ++
+   (formatSubline "creator" (Just contributor))
+
+
+subjectToString :: String -> String
+subjectToString = printf "subject: %s\n"
+
+
+descriptionToString :: Maybe String -> String
+descriptionToString = maybe "" (printf "description: %s\n")
+
+
+publisherToString :: Maybe String -> String
+publisherToString = maybe "" (printf "publisher: %s\n")
+
+
+dateToString :: EMDate -> String
+dateToString (EMDate Nothing date) =
+   printf "date: %s\n" date
+dateToString (EMDate event date) =
+   "date\n" ++
+   (formatSubline "event" event) ++
+   (formatSubline "date" (Just date))
+
+
+typeToString :: Maybe String -> String
+typeToString = maybe "" (printf "type: %s\n")
+
+
+formatToString :: Maybe String -> String
+formatToString = maybe "" (printf "format: %s\n")
+
+
+idToString :: EMId -> String
+idToString (EMId idVal scheme content) =
+   "identifier\n" ++
+   (formatSubline "id" (Just idVal)) ++
+   (formatSubline "scheme" scheme) ++
+   (formatSubline "identifier" (Just content))
+
+
+sourceToString :: Maybe String -> String
+sourceToString = maybe "" (printf "source: %s\n")
+
+
+langToString :: String -> String
+langToString = printf "language: %s\n"
+
+
+relationToString :: Maybe String -> String
+relationToString = maybe "" (printf "relation: %s\n")
+
+
+coverageToString :: Maybe String -> String
+coverageToString = maybe "" (printf "coverage: %s\n")
+
+
+rightsToString :: Maybe String -> String
+rightsToString = maybe "" (printf "rights: %s\n")
+
+
+-- | Format an ePub metadata into a String
+opfToString :: OPFPackage -> String
+opfToString (OPFPackage v u em) = concat $
+   [packageToString (v, u)] ++
+   (map titleToString $ emTitles em) ++
+   (map creatorToString $ emCreators em) ++
+   (map contributorToString $ emContributors em) ++
+   (map dateToString $ emDates em) ++
+   [typeToString . emType $ em] ++
+   [formatToString . emFormat $ em] ++
+   (map idToString $ emIds em) ++
+   [sourceToString . emSource $ em] ++
+   (map subjectToString $ emSubjects em) ++
+   [descriptionToString . emDescription $ em] ++
+   [publisherToString . emPublisher $ em] ++
+   (map langToString $ emLangs em) ++
+   [relationToString . emRelation $ em] ++
+   [coverageToString . emCoverage $ em] ++
+   [rightsToString . emRights $ em]
diff --git a/src/Codec/Epub/Opf/Metadata/Parse.hs b/src/Codec/Epub/Opf/Metadata/Parse.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Epub/Opf/Metadata/Parse.hs
@@ -0,0 +1,210 @@
+-- Copyright: 2010 Dino Morelli
+-- License: BSD3 (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+{-# LANGUAGE Arrows #-}
+{-# LANGUAGE FlexibleContexts #-}
+
+-- | Module for extracting the metadata from an ePub file
+module Codec.Epub.Opf.Metadata.Parse
+   ( parseXmlToOpf
+   , parseEpubOpf
+   )
+   where
+
+import Control.Applicative
+import Control.Monad.Error
+import Data.Tree.NTree.TypeDefs ( NTree )
+import Prelude hiding ( cos )
+import Text.XML.HXT.Arrow
+
+import Codec.Epub.IO
+import Codec.Epub.Opf.Metadata
+
+
+-- HXT helpers
+
+atTag :: (ArrowXml a) => String -> a (NTree XNode) XmlTree
+atTag tag = deep (isElem >>> hasName tag)
+
+atQTag :: (ArrowXml a) => QName -> a (NTree XNode) XmlTree
+atQTag tag = deep (isElem >>> hasQName tag)
+
+text :: (ArrowXml a) => a (NTree XNode) String
+text = getChildren >>> getText
+
+notNullA :: (ArrowList a) => a [b] [b]
+notNullA = isA $ not . null
+
+
+mbQTagText :: (ArrowXml a) => QName -> a (NTree XNode) (Maybe String)
+mbQTagText tag =
+   ( atQTag tag >>>
+     text >>> notNullA >>> arr Just )
+   `orElse`
+   (constA Nothing)
+
+
+mbGetAttrValue :: (ArrowXml a) =>
+   String -> a XmlTree (Maybe String)
+mbGetAttrValue n =
+   (getAttrValue n >>> notNullA >>> arr Just)
+   `orElse` (constA Nothing)
+
+mbGetQAttrValue :: (ArrowXml a) =>
+   QName -> a XmlTree (Maybe String)
+mbGetQAttrValue qn =
+   (getQAttrValue qn >>> notNullA >>> arr Just)
+   `orElse` (constA Nothing)
+
+
+{- ePub parsing helpers
+
+   Note that these URIs could conceivably change in the future
+   Is it ok that they're hardcoded like this?
+
+   Well, ok, the xml namespace URI will probably never change.
+-}
+
+dcName, opfName, xmlName :: String -> QName
+dcName local = mkQName "dc" local "http://purl.org/dc/elements/1.1/"
+opfName local = mkQName "opf" local "http://www.idpf.org/2007/opf"
+xmlName local = mkQName "xml" local "http://www.w3.org/XML/1998/namespace"
+
+
+getPackage :: (ArrowXml a) => a (NTree XNode) (String, String)
+getPackage = atTag "package" >>>
+   proc x -> do
+      v <- getAttrValue "version" -< x
+      u <- getAttrValue "unique-identifier" -< x
+      returnA -< (v, u)
+
+
+getTitle :: (ArrowXml a) => a (NTree XNode) EMTitle
+getTitle = atQTag (dcName "title") >>>
+   proc x -> do
+      l <- mbGetQAttrValue (xmlName "lang") -< x
+      c <- text -< x
+      returnA -< EMTitle l c
+
+
+{- Since creators and contributors have the same exact XML structure,
+   this arrow is used to get either of them
+-}
+getCreator :: (ArrowXml a) => String -> a (NTree XNode) EMCreator
+getCreator tag = atQTag (dcName tag) >>> ( unwrapArrow $ EMCreator
+   <$> (WrapArrow $ mbGetQAttrValue (opfName "role"))
+   <*> (WrapArrow $ mbGetQAttrValue (opfName "file-as"))
+   <*> (WrapArrow $ text)
+   )
+
+
+getSubject :: (ArrowXml a) => a (NTree XNode) String
+getSubject = atQTag (dcName "subject") >>> text
+
+
+getDescription :: (ArrowXml a) => a (NTree XNode) (Maybe String)
+getDescription = mbQTagText $ dcName "description"
+
+
+getPublisher :: (ArrowXml a) => a (NTree XNode) (Maybe String)
+getPublisher = mbQTagText $ dcName "publisher"
+
+
+getDate :: (ArrowXml a) => a (NTree XNode) EMDate
+getDate = atQTag (dcName "date") >>>
+   proc x -> do
+      e <- mbGetQAttrValue (opfName "event") -< x
+      c <- text -< x
+      returnA -< EMDate e c
+
+
+getType :: (ArrowXml a) => a (NTree XNode) (Maybe String)
+getType = mbQTagText $ dcName "type"
+
+
+getFormat :: (ArrowXml a) => a (NTree XNode) (Maybe String)
+getFormat = mbQTagText $ dcName "format"
+
+
+getId :: (ArrowXml a) => a (NTree XNode) EMId
+getId = atQTag (dcName "identifier") >>>
+   proc x -> do
+      mbi <- mbGetAttrValue "id" -< x
+      s <- mbGetQAttrValue (opfName "scheme") -< x
+      c <- text -< x
+      let i = maybe "[WARNING: missing required id attribute]" id mbi
+      returnA -< EMId i s c
+
+
+getSource :: (ArrowXml a) => a (NTree XNode) (Maybe String)
+getSource = mbQTagText $ dcName "source"
+
+
+getLang :: (ArrowXml a) => a (NTree XNode) String
+getLang = atQTag (dcName "language") >>> text
+
+
+getRelation :: (ArrowXml a) => a (NTree XNode) (Maybe String)
+getRelation = mbQTagText $ dcName "relation"
+
+
+getCoverage :: (ArrowXml a) => a (NTree XNode) (Maybe String)
+getCoverage = mbQTagText $ dcName "coverage"
+
+
+getRights :: (ArrowXml a) => a (NTree XNode) (Maybe String)
+getRights = mbQTagText $ dcName "rights"
+
+
+getMeta :: (ArrowXml a) => a (NTree XNode) EpubMeta
+getMeta = atTag "metadata" >>> ( unwrapArrow $ EpubMeta
+   <$> (WrapArrow $ listA getTitle)
+   <*> (WrapArrow $ listA $ getCreator "creator")
+   <*> (WrapArrow $ listA $ getCreator "contributor")
+   <*> (WrapArrow $ listA getSubject)
+   <*> (WrapArrow $ getDescription)
+   <*> (WrapArrow $ getPublisher)
+   <*> (WrapArrow $ listA getDate)
+   <*> (WrapArrow $ getType)
+   <*> (WrapArrow $ getFormat)
+   <*> (WrapArrow $ listA getId)
+   <*> (WrapArrow $ getSource)
+   <*> (WrapArrow $ listA getLang)
+   <*> (WrapArrow $ getRelation)
+   <*> (WrapArrow $ getCoverage)
+   <*> (WrapArrow $ getRights)
+   )
+
+
+getBookData :: (ArrowXml a) => a (NTree XNode) OPFPackage
+getBookData = 
+   proc x -> do
+      (v, u) <- getPackage -< x
+      m <- getMeta -< x
+      returnA -< OPFPackage v u m
+
+
+{- | Extract the ePub metadata contained in the OPF Package Document 
+   contained in the supplied string
+-}
+parseXmlToOpf :: (MonadIO m) => String -> m [OPFPackage]
+parseXmlToOpf opfContents =
+   liftIO $ runX (
+      readString [(a_validate, v_0)] opfContents
+      >>> propagateNamespaces
+      >>> getBookData
+      )
+
+
+-- | Given the path to an ePub file, extract the metadata
+parseEpubOpf :: (MonadIO m, MonadError String m) =>
+   FilePath -> m OPFPackage
+parseEpubOpf zipPath = do
+   opfContents <- extractFileFromZip zipPath =<< opfPath zipPath
+   result <- parseXmlToOpf opfContents
+
+   case result of
+      (em : []) -> return em
+      _         -> throwError
+         "ERROR: we didn't come up with a single EpubMeta"
diff --git a/src/Codec/Epub/Opf/epub-meta.hs b/src/Codec/Epub/Opf/epub-meta.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Epub/Opf/epub-meta.hs
@@ -0,0 +1,24 @@
+-- Copyright: 2010 Dino Morelli
+-- License: BSD3 (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+import Control.Monad.Error
+import System.Environment ( getArgs )
+import System.Exit
+
+import Codec.Epub.Opf.Metadata.Format
+import Codec.Epub.Opf.Metadata.Parse
+
+
+main :: IO ()
+main = do
+   as <- getArgs
+
+   when (length as /= 1) $ do
+      putStrLn "usage: epub-meta EPUBFILE"
+      exitWith $ ExitFailure 1
+
+   let zipPath = head as
+   result <- runErrorT $ parseEpubOpf zipPath
+
+   putStr $ either (++ "\n") opfToString result
diff --git a/testsuite/runtests.hs b/testsuite/runtests.hs
new file mode 100644
--- /dev/null
+++ b/testsuite/runtests.hs
@@ -0,0 +1,118 @@
+-- Copyright: 2010 Dino Morelli
+-- License: BSD3 (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+import System.FilePath
+import Test.HUnit ( Counts, Test (..), assertEqual, runTestTT )
+import Test.HUnit.Base ( Assertion )
+
+import Codec.Epub.Opf.Metadata
+import Codec.Epub.Opf.Metadata.Parse
+
+
+main = runTestTT tests >> return ()
+
+
+tests :: Test
+tests = TestList
+   [ testFull
+   , testMinimal
+   , testMissingAll
+   ]
+
+
+{- A fairly comprehensive test containing all possible things
+   Not complete at this time because the library can't parse it all yet!
+-}
+testFull :: Test
+testFull = TestCase $ do
+   xmlString <- readFile $ "testsuite" </> "testFull.opf"
+   actual <- parseXmlToOpf xmlString
+   let expected = [ OPFPackage "2.0" "isbn" ( EpubMeta
+         { emTitles =
+            [ EMTitle Nothing "Title Of This Book"
+            , EMTitle (Just "fr") "Titre De Ce Livre"
+            ]
+         , emCreators =
+            [ EMCreator
+               (Just "aut")
+               (Just "Wiggins, Josephine B.")
+               "Josephine B. Wiggins"
+            , EMCreator
+               (Just "aut")
+               Nothing
+               "Horatio Cromwell"
+            , EMCreator
+               Nothing
+               Nothing
+               "Natalia Jenkins"
+            ]
+         , emContributors =
+            [ EMCreator
+               (Just "ill")
+               (Just "Knickerbocker, Reginald Q.")
+               "Reginald Q. Knickerbocker"
+            , EMCreator
+               (Just "edt")
+               Nothing
+               "Beverly Abercrombie"
+            ]
+         , emSubjects = ["Fiction", "Science Fiction"]
+         , emDescription = Just "This document is a stub used for unit testing. It is missing the rest of the tags that normally occur after metadata."
+         , emPublisher = Just "Fictional Books Ltd."
+         , emDates =
+            [ EMDate (Just "published") "2010"
+            , EMDate (Just "created") "2010-05-07"
+            , EMDate (Just "modified") "2010-05-08T10:20:57"
+            , EMDate Nothing "2009-08-03T16:22:20"
+            ]
+         , emType = Just "test OPF Package Document"
+         , emFormat = Just "ePub publication"
+         , emIds =
+            [ EMId "isbn" (Just "ISBN") "1-82057-821-9"
+            , EMId "other" Nothing "1386506873266"]
+         , emSource = Just "document source"
+         , emLangs = ["en-us"]
+         , emRelation = Just "document relation"
+         , emCoverage = Just "coverage information"
+         , emRights = Just "Copyright: 2010 Dino Morelli, License: BSD3"
+         } ) ]
+   assertEqual "very full" expected actual
+
+
+{- Test the absolute minimum set of fields allowed while remaining 
+   compliant with the spec
+-}
+testMinimal :: Test
+testMinimal = TestCase $ do
+   xmlString <- readFile $ "testsuite" </> "testMinimal.opf"
+   actual <- parseXmlToOpf xmlString
+   let expected = [ OPFPackage "2.0" "isbn" ( EpubMeta
+         { emTitles = [EMTitle Nothing "Title Of This Book"]
+         , emCreators = []
+         , emContributors = []
+         , emSubjects = []
+         , emDescription = Nothing
+         , emPublisher = Nothing
+         , emDates = []
+         , emType = Nothing
+         , emFormat = Nothing
+         , emIds = [EMId "isbn" (Just "ISBN") "1-82057-821-9"]
+         , emSource = Nothing
+         , emLangs = ["en-us"]
+         , emRelation = Nothing
+         , emCoverage = Nothing
+         , emRights = Nothing
+         } ) ]
+   assertEqual "minimal" expected actual
+
+
+{- Test data missing everything important: package version and 
+   unique-identifier attributes, title, identifier and language tags
+-}
+testMissingAll :: Test
+testMissingAll = TestCase $ do
+   xmlString <- readFile $ "testsuite" </> "testMissingAll.opf"
+   actual <- parseXmlToOpf xmlString
+   let expected = [ OPFPackage "" "" emptyEpubMeta ]
+   assertEqual "missing all" expected actual
diff --git a/testsuite/testFull.opf b/testsuite/testFull.opf
new file mode 100644
--- /dev/null
+++ b/testsuite/testFull.opf
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package
+   xmlns="http://www.idpf.org/2007/opf"
+   version="2.0"
+   unique-identifier="isbn">
+   <metadata
+      xmlns:dc="http://purl.org/dc/elements/1.1/"
+      xmlns:opf="http://www.idpf.org/2007/opf"
+      xmlns:xml="http://www.w3.org/XML/1998/namespace">
+      <dc:title>Title Of This Book</dc:title>
+      <dc:title xml:lang="fr">Titre De Ce Livre</dc:title>
+      <dc:creator opf:role="aut" opf:file-as="Wiggins, Josephine B.">Josephine B. Wiggins</dc:creator>
+      <dc:creator opf:role="aut">Horatio Cromwell</dc:creator>
+      <dc:creator>Natalia Jenkins</dc:creator>
+      <dc:contributor opf:role="ill" opf:file-as="Knickerbocker, Reginald Q.">Reginald Q. Knickerbocker</dc:contributor>
+      <dc:contributor opf:role="edt">Beverly Abercrombie</dc:contributor>
+      <dc:description>This document is a stub used for unit testing. It is missing the rest of the tags that normally occur after metadata.</dc:description>
+      <dc:identifier id="isbn" opf:scheme="ISBN">1-82057-821-9</dc:identifier>
+      <dc:date opf:event="published">2010</dc:date>
+      <dc:date opf:event="created">2010-05-07</dc:date>
+      <dc:date opf:event="modified">2010-05-08T10:20:57</dc:date>
+      <dc:date>2009-08-03T16:22:20</dc:date>
+      <dc:language>en-us</dc:language>
+      <dc:publisher>Fictional Books Ltd.</dc:publisher>
+      <dc:identifier id="other">1386506873266</dc:identifier>
+      <dc:subject>Fiction</dc:subject>
+      <dc:subject>Science Fiction</dc:subject>
+      <dc:type>test OPF Package Document</dc:type>
+      <dc:format>ePub publication</dc:format>
+      <dc:source>document source</dc:source>
+      <dc:relation>document relation</dc:relation>
+      <dc:coverage>coverage information</dc:coverage>
+      <dc:rights>Copyright: 2010 Dino Morelli, License: BSD3</dc:rights>
+      <meta name="series_index" content="1"/>
+   </metadata>
+
+   <!-- This document is a stub used for unit testing. It is missing 
+        the rest of the tags that normally occur after metadata -->
+</package>
diff --git a/testsuite/testMinimal.opf b/testsuite/testMinimal.opf
new file mode 100644
--- /dev/null
+++ b/testsuite/testMinimal.opf
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package
+   xmlns="http://www.idpf.org/2007/opf"
+   version="2.0"
+   unique-identifier="isbn">
+   <metadata
+      xmlns:dc="http://purl.org/dc/elements/1.1/"
+      xmlns:opf="http://www.idpf.org/2007/opf"
+      xmlns:xml="http://www.w3.org/XML/1998/namespace">
+      <dc:title>Title Of This Book</dc:title>
+      <dc:identifier id="isbn" opf:scheme="ISBN">1-82057-821-9</dc:identifier>
+      <dc:language>en-us</dc:language>
+   </metadata>
+
+   <!-- This document is a stub used for unit testing. It is missing 
+        the rest of the tags that normally occur after metadata -->
+</package>
diff --git a/testsuite/testMissingAll.opf b/testsuite/testMissingAll.opf
new file mode 100644
--- /dev/null
+++ b/testsuite/testMissingAll.opf
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package xmlns="http://www.idpf.org/2007/opf">
+   <metadata
+      xmlns:dc="http://purl.org/dc/elements/1.1/"
+      xmlns:opf="http://www.idpf.org/2007/opf"
+      xmlns:xml="http://www.w3.org/XML/1998/namespace">
+   </metadata>
+
+   <!-- This document is a stub used for unit testing. It is missing 
+        the rest of the tags that normally occur after metadata -->
+</package>
diff --git a/util/gentags.sh b/util/gentags.sh
new file mode 100644
--- /dev/null
+++ b/util/gentags.sh
@@ -0,0 +1,3 @@
+#! /bin/sh
+
+find src -regex '.*\..?hs' | xargs hasktags -c
diff --git a/util/prefs/boring b/util/prefs/boring
new file mode 100644
--- /dev/null
+++ b/util/prefs/boring
@@ -0,0 +1,117 @@
+# Boring file regexps:
+
+### compiler and interpreter intermediate files
+# haskell (ghc) interfaces
+\.hi$
+\.hi-boot$
+\.o-boot$
+# object files
+\.o$
+\.o\.cmd$
+# profiling haskell
+\.p_hi$
+\.p_o$
+# haskell program coverage resp. profiling info
+\.tix$
+\.prof$
+# fortran module files
+\.mod$
+# linux kernel
+\.ko\.cmd$
+\.mod\.c$
+(^|/)\.tmp_versions($|/)
+# *.ko files aren't boring by default because they might
+# be Korean translations rather than kernel modules
+# \.ko$
+# python, emacs, java byte code
+\.py[co]$
+\.elc$
+\.class$
+# objects and libraries; lo and la are libtool things
+\.(obj|a|exe|so|lo|la)$
+# compiled zsh configuration files
+\.zwc$
+# Common LISP output files for CLISP and CMUCL
+\.(fas|fasl|sparcf|x86f)$
+
+### build and packaging systems
+# cabal intermediates
+\.installed-pkg-config
+\.setup-config
+# standard cabal build dir, might not be boring for everybody
+# ^dist(/|$)
+# autotools
+(^|/)autom4te\.cache($|/)
+(^|/)config\.(log|status)$
+# microsoft web expression, visual studio metadata directories
+\_vti_cnf$
+\_vti_pvt$
+# gentoo tools
+\.revdep-rebuild.*
+# generated dependencies
+^\.depend$
+
+### version control systems
+# cvs
+(^|/)CVS($|/)
+\.cvsignore$
+# cvs, emacs locks
+^\.#
+# rcs
+(^|/)RCS($|/)
+,v$
+# subversion
+(^|/)\.svn($|/)
+# mercurial
+(^|/)\.hg($|/)
+# git
+(^|/)\.git($|/)
+# bzr
+\.bzr$
+# sccs
+(^|/)SCCS($|/)
+# darcs
+(^|/)_darcs($|/)
+(^|/)\.darcsrepo($|/)
+^\.darcs-temp-mail$
+-darcs-backup[[:digit:]]+$
+# gnu arch
+(^|/)(\+|,)
+(^|/)vssver\.scc$
+\.swp$
+(^|/)MT($|/)
+(^|/)\{arch\}($|/)
+(^|/).arch-ids($|/)
+# bitkeeper
+(^|/)BitKeeper($|/)
+(^|/)ChangeSet($|/)
+
+### miscellaneous
+# backup files
+~$
+\.bak$
+\.BAK$
+# patch originals and rejects
+\.orig$
+\.rej$
+# X server
+\..serverauth.*
+# image spam
+\#
+(^|/)Thumbs\.db$
+# vi, emacs tags
+(^|/)(tags|TAGS)$
+#(^|/)\.[^/]
+# core dumps
+(^|/|\.)core$
+# partial broken files (KIO copy operations)
+\.part$
+# waf files, see http://code.google.com/p/waf/
+(^|/)\.waf-[[:digit:].]+-[[:digit:]]+($|/)
+(^|/)\.lock-wscript$
+# mac os finder
+(^|/)\.DS_Store$
+# cabal
+(^|/)dist($|/)
+
+(^|/)bin($|/)
