diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c)2012, Chris Done <chrisdone@gmail.com>
+
+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 Chris Done <chrisdone@gmail.com> 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/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/ety.cabal b/ety.cabal
new file mode 100644
--- /dev/null
+++ b/ety.cabal
@@ -0,0 +1,39 @@
+Name:                ety
+Version:             0.1
+Synopsis:            Random etymology online entry.
+Description:         Retrieve random etymology online entries. Library and program.
+License:             BSD3
+License-file:        LICENSE
+Author:              Chris Done <chrisdone@gmail.com>
+Maintainer:          Chris Done <chrisdone@gmail.com>
+Copyright:           Chris Done 2012
+Category:            Web
+Build-type:          Simple
+Cabal-version:       >=1.6
+
+Source-repository head
+  type: git
+  location: https://github.com/chrisdone/ety
+
+Library
+  Hs-source-dirs:   src
+  Exposed-modules:  Language.English.EtymologyOnline
+  Build-depends:    base > 4 && < 5,
+                    curl,
+                    xml,
+                    utf8-string,
+                    text-icu,
+                    random,
+                    bytestring
+
+Executable ety
+  Hs-source-dirs: src
+  Main-is:        Main.hs
+  Other-modules:  Language.English.EtymologyOnline
+  Build-depends:  base > 4 && < 5,
+                  curl,
+                  xml,
+                  utf8-string,
+                  text-icu,
+                  random,
+                  bytestring
diff --git a/src/Language/English/EtymologyOnline.hs b/src/Language/English/EtymologyOnline.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/English/EtymologyOnline.hs
@@ -0,0 +1,121 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+-- | An API for the Online Etymology Dictionary at http://etymonline.com/.
+--   The encoding is ISO-8859-1/latin1 so needs to use uconv.
+
+module Language.English.EtymologyOnline where
+
+import           Control.Exception
+import           Control.Monad
+import           Data.ByteString       (ByteString)
+import qualified Data.ByteString       as Bytes
+import           Data.ByteString.UTF8  (toString)
+import           Data.Maybe
+import           Data.Text.ICU.Convert
+import           Data.Typeable
+import           Network.Curl
+import           System.Random
+import           Text.XML.Light
+
+-- | An EO exception.
+data EOException
+  = CannotParsePageCountForLetter Char
+  | CannotDownloadPageCountForLetter Char
+  | CannotParsePageEntries Char Int (Maybe Element)
+  | CannotDownloadPageEntries Char Int
+  | NoEntryInPage Char Int
+  deriving (Typeable,Show)
+instance Exception EOException
+
+-- | Get a random Etymology Online entry.
+getRandomEntry :: IO (String,String)
+getRandomEntry = do
+  converter <- open "windows-1252" Nothing
+  letter <- randomRIO ('a','z')
+  pages <- getletterpages converter letter
+  pagenumber <- randomRIO (1,pages)
+  entries <- getletterentries converter letter pagenumber
+  index <- randomRIO (1,length entries)
+  maybe (throw (NoEntryInPage letter pagenumber))
+        return
+        (lookup index (zip [1..] entries))
+
+-- | Get the number of pages for a letter.
+getletterpages :: Converter -> Char -> IO Int
+getletterpages converter l = withCurlDo $ do
+  (code,bytes) <- curlGetString_ url []
+  case code of
+    CurlOK -> do
+      let doc = parseEOXML converter (bytes :: ByteString)
+      maybe (return 0) -- Some pages don't have pagination at all.
+            (return . length)
+            (getlinks doc)
+    _ -> throw (CannotDownloadPageCountForLetter l)
+  where url = "http://www.etymonline.com/index.php?l=" ++ [l]
+
+-- | Get the entries for a letter and page number.
+getletterentries :: Converter -> Char -> Int -> IO [(String,String)]
+getletterentries converter l pn = withCurlDo $ do
+  (code,bytes) <- curlGetString_ url [] 
+  case code of
+    CurlOK -> do
+      let doc = parseEOXML converter (bytes :: ByteString)
+      maybe (throw (CannotParsePageEntries l pn doc)) return (getentries doc)
+    _ -> throw (CannotDownloadPageEntries l pn)
+  where url = "http://www.etymonline.com/index.php?l=" ++ [l] ++ "&p=" ++ show (pn - 1)
+
+-- Get the entries given the page.
+getentries :: Maybe Element -> Maybe [(String, String)]
+getentries doc = do
+  el <- doc
+  let dts = grab "dt" el
+      dds = grab "dd" el
+  return (zip dts dds)
+    
+  where grab key = map (trim . allContent)
+                 . findElements (qname key xhtml)
+
+-- Get the pagination links from the page.
+getlinks :: Maybe Element -> Maybe [String]
+getlinks doc = do
+  el <- doc
+  paging <- filterElement paginglist el
+  ul <- findChild (qname "ul" xhtml) paging
+  return (paginglinks ul)
+
+-- Find the element with the pagination.
+paginglist :: Element -> Bool
+paginglist el = findAttr (qname "class" Nothing) el == Just "paging"
+
+-- Get the pagination links.
+paginglinks :: Element -> [String]
+paginglinks = mapMaybe (findAttr (qname "href" Nothing))
+            . getels "a"
+              
+  where getels key = findElements (qname key xhtml)
+
+-- Get all text content of an element.
+allContent :: Element -> String
+allContent (Element _ _ contents _) = concatMap switch contents where
+  switch el =
+    case el of
+      Elem e -> allContent e
+      CRef _  -> ""
+      Text (CData _ d _) -> d
+
+-- Parse an ISO-8859-1 document to an XML tree.
+parseEOXML :: Converter -> ByteString -> Maybe Element
+parseEOXML converter =
+  parseXMLDoc . toUnicode converter
+
+-- Some utils.
+      
+xhtml :: Maybe String
+xhtml = Just "http://www.w3.org/1999/xhtml"
+
+qname :: String -> Maybe String -> QName
+qname q uri = blank_name { qName = q, qURI = uri }
+
+trim :: String -> String
+trim = unwords . words
diff --git a/src/Main.hs b/src/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/Main.hs
@@ -0,0 +1,7 @@
+import Language.English.EtymologyOnline
+
+main :: IO ()
+main = do
+  (title,entry) <- getRandomEntry
+  putStrLn title
+  putStrLn entry
