about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--res/.DS_Storebin0 -> 6148 bytes
-rw-r--r--res/admin.css49
-rw-r--r--res/apple-touch-icon.pngbin0 -> 9756 bytes
-rw-r--r--res/blogstyle.css99
-rw-r--r--res/drama.wavbin0 -> 208300 bytes
-rw-r--r--res/favicon.icobin0 -> 4354 bytes
-rw-r--r--res/loginBoxTop.pngbin0 -> 606 bytes
-rw-r--r--res/signin.gifbin0 -> 1850 bytes
-rw-r--r--res/twtbtn.pngbin0 -> 8058 bytes
-rw-r--r--src/.DS_Storebin0 -> 6148 bytes
-rw-r--r--src/Blog.hs33
-rw-r--r--src/Server.hs98
-rw-r--r--tools/.DS_Storebin0 -> 6148 bytes
-rw-r--r--tools/convertdb/.DS_Storebin0 -> 6148 bytes
-rw-r--r--tools/convertdb/Makefile13
-rw-r--r--tools/convertdb/convertdb.go121
-rw-r--r--tools/convertdb/couch.go403
-rw-r--r--tools/music/Makefile10
-rwxr-xr-xtools/music/gettitle4
-rw-r--r--tools/music/iTunes.8bin0 -> 37608 bytes
-rw-r--r--tools/music/iTunes.go79
-rwxr-xr-xtools/music/musicbin0 -> 2843022 bytes
-rwxr-xr-xtools/music/start1
23 files changed, 910 insertions, 0 deletions
diff --git a/res/.DS_Store b/res/.DS_Store
new file mode 100644
index 000000000000..5008ddfcf53c
--- /dev/null
+++ b/res/.DS_Store
Binary files differdiff --git a/res/admin.css b/res/admin.css
new file mode 100644
index 000000000000..7a4d418975ca
--- /dev/null
+++ b/res/admin.css
@@ -0,0 +1,49 @@
+@charset "UTF-8";
+/* CSS Document */
+
+
+body {
+	padding-top: 20px;
+	font-family: 'PT Sans', sans-serif;
+	background-image: linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%);
+	background-image: -o-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%);
+	background-image: -webkit-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%);
+	background-image: -webkit-gradient(
+		linear,
+		left bottom,
+		left top,
+		color-stop(0.66, rgb(245,245,245)),
+		color-stop(0.83, rgb(239,239,239))
+	);
+	background-repeat: no-repeat;
+	background-color: rgb(245,245,245);
+}
+
+.loginBox {	
+	width: 400px;
+	margin: 0 auto;
+}
+
+.loginBoxTop {
+	width: 380px;
+	height: 28px;
+	color: #FFFFFF;
+	font-size: 12px;
+	padding-left: 20px;
+	padding-top: 11px;
+	background: url(/res/loginBoxTop.png);
+}
+
+.loginBoxMiddle {
+    background-color: #F3F3F3;
+    border-top: 0px hidden;
+    border:1px solid #D2D2D2;
+    border-bottom-left-radius: 12px;
+    border-bottom-right-radius: 12px;
+	;
+    font-size:12px;
+    height:auto;
+    min-height:200px;
+    padding-left:20px;
+    width:378px;
+}
\ No newline at end of file
diff --git a/res/apple-touch-icon.png b/res/apple-touch-icon.png
new file mode 100644
index 000000000000..22ba058cddd4
--- /dev/null
+++ b/res/apple-touch-icon.png
Binary files differdiff --git a/res/blogstyle.css b/res/blogstyle.css
new file mode 100644
index 000000000000..8c065c53ce20
--- /dev/null
+++ b/res/blogstyle.css
@@ -0,0 +1,99 @@
+@charset "UTF-8";
+/* CSS Document */
+
+@font-face {
+  font-family: 'PT Sans';
+  font-style: normal;
+  font-weight: normal;
+  src: local('PT Sans'), local('PTSans-Regular'), url('http://themes.googleusercontent.com/static/fonts/ptsans/v2/7YGmE4Ls5b94ct65u07hVQLUuEpTyoUstqEm5AMlJo4.woff') format('woff');
+}
+
+body {
+	padding-top: 20px;
+	font-family: 'PT Sans', sans-serif;
+	background-image: linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%);
+	background-image: -o-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%);
+	background-image: -moz-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%);
+	background-image: -webkit-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%);
+	background-image: -ms-linear-gradient(bottom, rgb(245,245,245) 66%, rgb(239,239,239) 83%);
+	background-image: -webkit-gradient(
+		linear,
+		left bottom,
+		left top,
+		color-stop(0.66, rgb(245,245,245)),
+		color-stop(0.83, rgb(239,239,239))
+	);
+}
+
+.mainshell {
+	width: 98%;
+	margin: auto;
+}
+
+.gradBox {
+	width: 98%;
+	margin: auto;
+}
+
+.myclear {
+	clear: both;
+}
+
+.centerbox {
+	text-align:center;
+}
+
+.innerBox {
+	width: 100%;
+	margin-top: 20px;
+}
+
+.innerBoxTop {
+	height: 28px;
+	color: #000000;
+	font-weight: bold;
+	font-size: 16px;
+	padding-left: 20px;
+	padding-top: 11px;
+	border: 1px solid #b6b6b6;
+	border-top-left-radius: 6px;
+	border-top-right-radius: 6px;
+	-moz-border-radius-topleft: 6px;
+	-moz-border-radius-topright: 6px;
+	-webkit-border-top-left-radius: 6px;
+	-webkit-border-top-right-radius: 6px;	
+	border-bottom: 1px solid #dcdcdc;
+	background-image: linear-gradient(bottom, rgb(246,246,246) 31%, rgb(234,234,234) 83%);
+	background-image: -o-linear-gradient(bottom, rgb(246,246,246) 31%, rgb(234,234,234) 83%);
+	background-image: -moz-linear-gradient(bottom, rgb(246,246,246) 31%, rgb(234,234,234) 83%);
+	background-image: -webkit-linear-gradient(bottom, rgb(246,246,246) 31%, rgb(234,234,234) 83%);
+	background-image: -ms-linear-gradient(bottom, rgb(246,246,246) 31%, rgb(234,234,234) 83%);
+	
+	background-image: -webkit-gradient(
+		linear,
+		left bottom,
+		left top,
+		color-stop(0.31, rgb(246,246,246)),
+		color-stop(0.83, rgb(234,234,234))
+	);
+}
+
+.innerBoxMiddle {
+	border: 1px solid #b6b6b6;
+	border-bottom-left-radius: 6px;
+	border-bottom-right-radius: 6px;
+	-moz-border-radius-bottomleft: 6px;
+	-moz-border-radius-bottomright: 6px;
+	-webkit-border-bottom-left-radius: 6px;
+	-webkit-border-bottom-right-radius: 6px;
+	border-top: 0px hidden;
+	background-color: #FFFFFF;
+	min-height: 200px;
+	height: auto;
+	padding-top: 21px;
+	padding-right: 2px;
+}
+
+.innerBoxComments {
+	padding-left: 20px
+}
\ No newline at end of file
diff --git a/res/drama.wav b/res/drama.wav
new file mode 100644
index 000000000000..20d326c5f40b
--- /dev/null
+++ b/res/drama.wav
Binary files differdiff --git a/res/favicon.ico b/res/favicon.ico
new file mode 100644
index 000000000000..2958dd3afcb0
--- /dev/null
+++ b/res/favicon.ico
Binary files differdiff --git a/res/loginBoxTop.png b/res/loginBoxTop.png
new file mode 100644
index 000000000000..8a0ee3ba8d6f
--- /dev/null
+++ b/res/loginBoxTop.png
Binary files differdiff --git a/res/signin.gif b/res/signin.gif
new file mode 100644
index 000000000000..bbe282bae0a4
--- /dev/null
+++ b/res/signin.gif
Binary files differdiff --git a/res/twtbtn.png b/res/twtbtn.png
new file mode 100644
index 000000000000..3a54c73c4c2c
--- /dev/null
+++ b/res/twtbtn.png
Binary files differdiff --git a/src/.DS_Store b/src/.DS_Store
new file mode 100644
index 000000000000..db284a629a24
--- /dev/null
+++ b/src/.DS_Store
Binary files differdiff --git a/src/Blog.hs b/src/Blog.hs
new file mode 100644
index 000000000000..2a62bb768072
--- /dev/null
+++ b/src/Blog.hs
@@ -0,0 +1,33 @@
+{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-}
+module Blog where
+
+import           Text.Blaze (toValue, preEscapedString)
+import           Text.Blaze.Html5 (Html, (!), a, form, input, p, toHtml, label)
+import           Text.Blaze.Html5.Attributes (action, enctype, href, name, size, type_, value)
+import qualified Text.Blaze.Html5 as H
+import qualified Text.Blaze.Html5.Attributes as A
+
+blogTemplate :: String -> String -> String -> Html
+blogTemplate t h o = H.docTypeHtml $ do
+    H.head $ do
+        H.title $ (toHtml t)
+        H.link ! A.rel "alternate" ! A.type_ "application/rss+xml" ! A.title "RSS-Feed" ! A.href "/rss"
+        H.link ! A.rel "stylesheet" ! A.type_ "text/css" ! A.href "/res/blogstyle.css" ! A.media "all"
+        H.meta ! A.httpEquiv "content-type" ! A.content "text/html;charset=UTF-8"
+{-        H.style ! A.type_ "text/css" ! A.title "iOS iMessage" ! A.media "screen and (max-device-width: 1024px)" $ "#cosx{display:none;} #cios{display:block;}" -}
+    H.body $ do
+        H.div ! A.class_ "mainshell" $ H.div ! A.class_ "gradBox" $ H.div ! A.class_ "header" $ do
+                H.a ! A.href "/" ! A.style "text-decoration:none;color:black;font-size:x-large;font-weight:bold;" $
+                        (toHtml t)
+                H.br
+                H.span ! A.id "cosx" ! A.style "display:block;" $ H.b $ contactInfo "imessage:tazjin@me.com"
+                H.span ! A.id "cios" ! A.style "display:none;" $ H.b $ contactInfo "sms:tazjin@me.com"
+    where
+        contactInfo (imu :: String) = do
+            toHtml h
+            H.a ! A.href "mailto:hej@tazj.in" $ "Mail"
+            ", "
+            H.a ! A.href "http://twitter.com/#!/tazjin" ! A.target "_blank" $ "Twitter"
+            toHtml o
+            H.a ! A.href (toValue imu) ! A.target "_blank" $ "iMessage"
+            "."
\ No newline at end of file
diff --git a/src/Server.hs b/src/Server.hs
new file mode 100644
index 000000000000..aa41a2173d6e
--- /dev/null
+++ b/src/Server.hs
@@ -0,0 +1,98 @@
+{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, DeriveDataTypeable #-}
+
+module Main where
+
+import			 Control.Monad (msum, mzero)
+import 			 Data.Data (Data, Typeable)
+import			 Data.Monoid (mempty)
+import			 Data.ByteString.Char8 (ByteString)
+import 			 Data.Text hiding (map, length, zip, head)
+import			 Data.Time
+import			 Database.CouchDB
+import			 Happstack.Server
+import           Text.Blaze (toValue, preEscapedString)
+import           Text.Blaze.Html5 (Html, (!), a, form, input, p, toHtml, label)
+import           Text.Blaze.Html5.Attributes (action, enctype, href, name, size, type_, value)
+import qualified Text.Blaze.Html5 as H
+import qualified Text.Blaze.Html5.Attributes as A
+import			 Text.JSON.Generic
+
+import			 Blog
+
+tmpPolicy :: BodyPolicy
+tmpPolicy = (defaultBodyPolicy "./tmp/" 0 1000 1000)
+
+data BlogLang = EN | DE
+
+data Comment = Comment{
+	cauthor	:: String,
+	ctext	:: String,
+	cdate	:: Integer
+} deriving (Show, Data, Typeable)
+
+data Entry = Entry{
+	_id		 :: String,
+	year 	 :: Int,
+	month 	 :: Int,
+	day 	 :: Int,
+	lang 	 :: String,
+	title 	 :: String,
+	author   :: String,
+	text 	 :: String,
+	mtext 	 :: String,
+	comments :: [Comment]
+} deriving (Show, Data, Typeable)
+
+instance Show BlogLang where
+	show EN = "en"
+	show DE = "de"
+
+--TazBlog version
+version = ("2.2b" :: String)
+
+main :: IO()
+main = do
+	putStrLn ("TazBlog " ++ version ++ " in Haskell starting")
+	simpleHTTP nullConf tazBlog
+
+tazBlog :: ServerPart Response
+tazBlog = do
+	msum [ dir "en" $ blogHandler EN
+		 , dir "de" $ blogHandler DE
+		 , do nullDir;
+		 	  ok $ showIndex DE
+		 , do dir " " $ nullDir;
+		 	  seeOther ("https://plus.google.com/115916629925754851590" :: String) (toResponse ())
+		 , dir "res" $ serveDirectory DisableBrowsing [] "../res"
+		 , serveDirectory DisableBrowsing [] "../res"
+		 ]
+
+blogHandler :: BlogLang -> ServerPart Response
+blogHandler lang = 
+	msum [ path $ \(year :: Int) -> path $ \(month :: Int) -> path $ 
+					  \(day :: Int) -> path $ \(id_ :: String) -> showEntry lang year month day id_
+		 ]
+
+showEntry :: BlogLang -> Int -> Int -> Int -> String -> ServerPart Response
+showEntry EN y m d i = undefined
+showEntry DE y m d i = undefined
+
+showIndex :: BlogLang -> Response
+showIndex lang = toResponse $ renderBlogHeader lang
+
+renderBlogHeader :: BlogLang -> Html
+renderBlogHeader DE = blogTemplate "Tazjins Blog" "Wer mich kontaktieren will: " " oder " 
+renderBlogHeader EN = blogTemplate "Tazjin's Blog" "Get in touch with me: " " or "
+
+-- http://tazj.in/2012/02/10.155234
+
+-- CouchDB View Setup
+latestDEView = "function(doc){ if(doc.lang == \"de\"){ emit([doc.year, doc.month, doc.day, doc.id_], doc); } }"
+latestENView = "function(doc){ if(doc.lang == \"en\"){ emit([doc.year, doc.month, doc.day, doc.id_]], doc); } }"
+
+latestDE = ViewMap "latestDE" latestDEView
+latestEN = ViewMap "latestEN" latestENView
+
+setupBlogViews :: IO () -- taking *reservations* DB name as parameter because we'll have multiple stores
+setupBlogViews = runCouchDB' $ 
+    newView "tazblog" "entries" [latestDE, latestEN]
diff --git a/tools/.DS_Store b/tools/.DS_Store
new file mode 100644
index 000000000000..714b6567ae00
--- /dev/null
+++ b/tools/.DS_Store
Binary files differdiff --git a/tools/convertdb/.DS_Store b/tools/convertdb/.DS_Store
new file mode 100644
index 000000000000..5de3023c9b98
--- /dev/null
+++ b/tools/convertdb/.DS_Store
Binary files differdiff --git a/tools/convertdb/Makefile b/tools/convertdb/Makefile
new file mode 100644
index 000000000000..eba288ec1d5b
--- /dev/null
+++ b/tools/convertdb/Makefile
@@ -0,0 +1,13 @@
+all: convertdb
+
+convertdb: couch.8 convertdb.8
+			8l -o convertdb convertdb.8
+
+convertdb.8: convertdb.go
+	8g convertdb.go
+
+couch.8: couch.go
+	8g couch.go
+
+clean:
+	rm -rf *.8 convertdb	
diff --git a/tools/convertdb/convertdb.go b/tools/convertdb/convertdb.go
new file mode 100644
index 000000000000..adef31910bf1
--- /dev/null
+++ b/tools/convertdb/convertdb.go
@@ -0,0 +1,121 @@
+package main
+
+import (
+	"strconv"
+	"fmt"
+	"io/ioutil"
+	"json"
+	"./couch"
+	"os"
+	"time"
+)
+
+//old
+type OldComment struct {
+	Author string
+	Text   string
+	Date   string
+}
+
+type OldEntry struct {
+	Id       string
+	Title    string
+	Author   string
+	Text     string
+	Mtext    string
+	Comments []OldComment
+}
+
+//new
+type Comment struct {
+	Author string 	`json:"cauthor"`
+	Text   string 	`json:"ctext"`
+	Date   int64 	`json:"cdate"`
+}
+
+type Entry struct {
+	Id       string `json:"_id"`
+	Year     int    `json:"year"`
+	Month    int    `json:"month"`
+	Day		 int
+	Lang     string `json:"lang"`
+	Title    string `json:"title"`
+	Author   string `json:"author"`
+	Text     string `json:"text"`
+	Mtext    string `json:"mtext"`
+	Comments []Comment 	`json:"comments"`
+}
+
+func main() {
+	getAllByYear("2011", 8, 12)
+	getAllByYear("2012", 1, 2)
+}
+
+func getAllByYear(year string, minm, maxm int){
+	db, _ := couch.NewDatabase("127.0.0.1", "5984", "tazblog")
+	for i:=minm;i<=maxm;i++{
+		dirList, err := ioutil.ReadDir(fmt.Sprintf("data/%s/%02d/", year, i))
+		if err != nil {
+			fmt.Println(err.String())
+			os.Exit(1)
+		}
+		for d:=len(dirList)-1; d>-1; d--{
+			content, cErr := ioutil.ReadFile(fmt.Sprintf("data/%s/%02d/%s", year, i, dirList[d].Name))
+			if cErr != nil {
+				fmt.Println(cErr)
+				os.Exit(1)
+			}
+			var oEntry OldEntry
+			jErr := json.Unmarshal(content, &oEntry)
+			if jErr != nil {
+				fmt.Println(jErr.String())
+				os.Exit(1)
+			}
+			nEntry := convertEntry(oEntry, fmt.Sprintf("data/%s/%02d/%s", year, i, dirList[d].Name))
+			eId, _, err := db.Insert(nEntry)
+			if err != nil {
+				fmt.Println(err.String())
+				os.Exit(1)
+			}
+			fmt.Println("Inserted " + eId)
+		}		
+	}
+}
+
+func convertEntry(oEntry OldEntry, p string) Entry{
+	var nEntry Entry
+	nComments := make([]Comment, len(oEntry.Comments))
+	for i:=0;i<len(oEntry.Comments);i++{
+		nComments[i].Author = oEntry.Comments[i].Author
+		nComments[i].Text = oEntry.Comments[i].Text
+		nComments[i].Date = parseDumbTime(oEntry.Comments[i].Date)
+	}
+
+	nEntry.Id 		= oEntry.Id[3:]
+	nEntry.Year, _  = strconv.Atoi(p[5:9])
+	nEntry.Month, _ = strconv.Atoi(p[10:12])
+	nEntry.Day, _	= strconv.Atoi(p[13:15])
+	nEntry.Title  	= oEntry.Title
+	nEntry.Author 	= oEntry.Author
+	nEntry.Mtext 	= oEntry.Mtext
+	nEntry.Text 	= oEntry.Text
+	nEntry.Comments = nComments
+	nEntry.Lang 	= "de"
+
+	return nEntry
+}
+
+//http://tazj.in/2012/02/10.155234
+func parseEntryTime(year, month, day int, ids string) string {
+	x := fmt.Sprintf()
+}
+
+func parseDumbTime(Year, Month, Day int, ) int64 {
+	x, err := time.Parse("[Am 02.01.2006 um 15:04 Uhr]", ct)
+	if err != nil {
+		fmt.Println(err.String())
+		os.Exit(1)
+	}
+
+	return x.Seconds()
+}
diff --git a/tools/convertdb/couch.go b/tools/convertdb/couch.go
new file mode 100644
index 000000000000..764eb49a6099
--- /dev/null
+++ b/tools/convertdb/couch.go
@@ -0,0 +1,403 @@
+// -*- tab-width: 4 -*-
+package couch
+
+import (
+	"bytes"
+	"fmt"
+	"os"
+	"json"
+	"http"
+	"net"
+	"io/ioutil"
+	"url"
+)
+
+var def_hdrs = map[string][]string{}
+
+type buffer struct {
+	b *bytes.Buffer
+}
+
+func (b *buffer) Read(out []byte) (int, os.Error) {
+	return b.b.Read(out)
+}
+
+func (b *buffer) Close() os.Error { return nil }
+
+// Converts given URL to string containing the body of the response.
+func url_to_buf(u string) []byte {
+	if r, err := http.Get(u); err == nil {
+		b, err := ioutil.ReadAll(r.Body)
+		r.Body.Close()
+		if err == nil {
+			return b
+		}
+	}
+	return make([]byte, 0)
+}
+
+type IdAndRev struct {
+	Id  string `json:"_id"`
+	Rev string `json:"_rev"`
+}
+
+// Sends a query to CouchDB and parses the response back.
+// method: the name of the HTTP method (POST, PUT,...)
+// url: the URL to interact with
+// headers: additional headers to pass to the request
+// in: body of the request
+// out: a structure to fill in with the returned JSON document
+func (p Database) interact(method, u string, headers map[string][]string, in []byte, out interface{}) (int, os.Error) {
+	fullHeaders := map[string][]string{}
+	for k, v := range headers {
+		fullHeaders[k] = v
+	}
+	bodyLength := 0
+	if in != nil {
+		bodyLength = len(in)
+		fullHeaders["Content-Type"] = []string{"application/json"}
+	}
+	req := http.Request{
+		Method:        method,
+		ProtoMajor:    1,
+		ProtoMinor:    1,
+		Close:         true,
+		ContentLength: int64(bodyLength),
+		Header:        fullHeaders,
+	}
+	req.TransferEncoding = []string{"chunked"}
+	req.URL, _ = url.Parse(u)
+	if in != nil {
+		req.Body = &buffer{bytes.NewBuffer(in)}
+	}
+	conn, err := net.Dial("tcp", fmt.Sprintf("%s:%s", p.Host, p.Port))
+	if err != nil {
+		return 0, err
+	}
+	http_conn := http.NewClientConn(conn, nil)
+	defer http_conn.Close()
+	if err := http_conn.Write(&req); err != nil {
+		return 0, err
+	}
+	r, err := http_conn.Read(&req)
+	if err != nil {
+		return 0, err
+	}
+	if r.StatusCode < 200 || r.StatusCode >= 300 {
+		b := []byte{}
+		r.Body.Read(b)
+		return r.StatusCode, os.NewError("server said: " + r.Status)
+	}
+	decoder := json.NewDecoder(r.Body)
+	if err = decoder.Decode(out); err != nil {
+		return 0, err
+	}
+	r.Body.Close()
+	return r.StatusCode, nil
+}
+
+type Database struct {
+	Host string
+	Port string
+	Name string
+}
+
+func (p Database) BaseURL() string {
+	return fmt.Sprintf("http://%s:%s", p.Host, p.Port)
+}
+
+func (p Database) DBURL() string {
+	return fmt.Sprintf("%s/%s", p.BaseURL(), p.Name)
+}
+
+// Test whether CouchDB is running (ignores Database.Name)
+func (p Database) Running() bool {
+	u := fmt.Sprintf("%s/%s", p.BaseURL(), "_all_dbs")
+	s := url_to_buf(u)
+	if len(s) > 0 {
+		return true
+	}
+	return false
+}
+
+type database_info struct {
+	Db_name string
+	// other stuff too, ignore for now
+}
+
+// Test whether specified database exists in specified CouchDB instance
+func (p Database) Exists() bool {
+	di := new(database_info)
+	if err := json.Unmarshal(url_to_buf(p.DBURL()), di); err != nil {
+		return false
+	}
+	if di.Db_name != p.Name {
+		return false
+	}
+	return true
+}
+
+func (p Database) create_database() os.Error {
+	ir := response{}
+	if _, err := p.interact("PUT", p.DBURL(), def_hdrs, nil, &ir); err != nil {
+		return err
+	}
+	if !ir.Ok {
+		return os.NewError("Create database operation returned not-OK")
+	}
+	return nil
+}
+
+// Deletes the given database and all documents
+func (p Database) DeleteDatabase() os.Error {
+	ir := response{}
+	if _, err := p.interact("DELETE", p.DBURL(), def_hdrs, nil, &ir); err != nil {
+		return err
+	}
+	if !ir.Ok {
+		return os.NewError("Delete database operation returned not-OK")
+	}
+	return nil
+}
+
+func NewDatabase(host, port, name string) (Database, os.Error) {
+	db := Database{host, port, name}
+	if !db.Running() {
+		return db, os.NewError("CouchDB not running")
+	}
+	if !db.Exists() {
+		if err := db.create_database(); err != nil {
+			return db, err
+		}
+	}
+	return db, nil
+}
+
+// Strip _id and _rev from d, returning them separately if they exist
+func clean_JSON(d interface{}) (json_buf []byte, id, rev string, err os.Error) {
+	json_buf, err = json.Marshal(d)
+	if err != nil {
+		return
+	}
+	m := map[string]interface{}{}
+	err = json.Unmarshal(json_buf, &m)
+	if err != nil {
+		return
+	}
+	id_rev := new(IdAndRev)
+	err = json.Unmarshal(json_buf, &id_rev)
+	if err != nil {
+		return
+	}
+	if _, ok := m["_id"]; ok {
+		id = id_rev.Id
+		m["_id"] = nil, false
+	}
+	if _, ok := m["_rev"]; ok {
+		rev = id_rev.Rev
+		m["_rev"] = nil, false
+	}
+	json_buf, err = json.Marshal(m)
+	return
+}
+
+type response struct {
+	Ok     bool
+	Id     string
+	Rev    string
+	Error  string
+	Reason string
+}
+
+// Inserts document to CouchDB, returning id and rev on success.
+// Document may specify both "_id" and "_rev" fields (will overwrite existing)
+//	or just "_id" (will use that id, but not overwrite existing)
+//	or neither (will use autogenerated id)
+func (p Database) Insert(d interface{}) (string, string, os.Error) {
+	json_buf, id, rev, err := clean_JSON(d)
+	if err != nil {
+		return "", "", err
+	}
+	if id != "" && rev != "" {
+		new_rev, err2 := p.Edit(d)
+		return id, new_rev, err2
+	} else if id != "" {
+		return p.insert_with(json_buf, id)
+	} else if id == "" {
+		return p.insert(json_buf)
+	}
+	return "", "", os.NewError("invalid Document")
+}
+
+// Private implementation of simple autogenerated-id insert
+func (p Database) insert(json_buf []byte) (string, string, os.Error) {
+	ir := response{}
+	if _, err := p.interact("POST", p.DBURL(), def_hdrs, json_buf, &ir); err != nil {
+		return "", "", err
+	}
+	if !ir.Ok {
+		return "", "", os.NewError(fmt.Sprintf("%s: %s", ir.Error, ir.Reason))
+	}
+	return ir.Id, ir.Rev, nil
+}
+
+// Inserts the given document (shouldn't contain "_id" or "_rev" tagged fields)
+// using the passed 'id' as the _id. Will fail if the id already exists.
+func (p Database) InsertWith(d interface{}, id string) (string, string, os.Error) {
+	json_buf, err := json.Marshal(d)
+	if err != nil {
+		return "", "", err
+	}
+	return p.insert_with(json_buf, id)
+}
+
+// Private implementation of insert with given id
+func (p Database) insert_with(json_buf []byte, id string) (string, string, os.Error) {
+	u := fmt.Sprintf("%s/%s", p.DBURL(), url.QueryEscape(id))
+	ir := response{}
+	if _, err := p.interact("PUT", u, def_hdrs, json_buf, &ir); err != nil {
+		return "", "", err
+	}
+	if !ir.Ok {
+		return "", "", os.NewError(fmt.Sprintf("%s: %s", ir.Error, ir.Reason))
+	}
+	return ir.Id, ir.Rev, nil
+}
+
+// Edits the given document, returning the new revision.
+// d must contain "_id" and "_rev" tagged fields.
+func (p Database) Edit(d interface{}) (string, os.Error) {
+	json_buf, err := json.Marshal(d)
+	if err != nil {
+		return "", err
+	}
+	id_rev := new(IdAndRev)
+	err = json.Unmarshal(json_buf, id_rev)
+	if err != nil {
+		return "", err
+	}
+	if id_rev.Id == "" {
+		return "", os.NewError("Id not specified in interface")
+	}
+	if id_rev.Rev == "" {
+		return "", os.NewError("Rev not specified in interface (try InsertWith)")
+	}
+	u := fmt.Sprintf("%s/%s", p.DBURL(), url.QueryEscape(id_rev.Id))
+	ir := response{}
+	if _, err = p.interact("PUT", u, def_hdrs, json_buf, &ir); err != nil {
+		return "", err
+	}
+	return ir.Rev, nil
+}
+
+// Edits the given document, returning the new revision.
+// d should not contain "_id" or "_rev" tagged fields. If it does, they will
+// be overwritten with the passed values.
+func (p Database) EditWith(d interface{}, id, rev string) (string, os.Error) {
+	if id == "" || rev == "" {
+		return "", os.NewError("EditWith: must specify both id and rev")
+	}
+	json_buf, err := json.Marshal(d)
+	if err != nil {
+		return "", err
+	}
+	m := map[string]interface{}{}
+	err = json.Unmarshal(json_buf, &m)
+	if err != nil {
+		return "", err
+	}
+	m["_id"] = id
+	m["_rev"] = rev
+	return p.Edit(m)
+}
+
+// Unmarshals the document matching id to the given interface, returning rev.
+func (p Database) Retrieve(id string, d interface{}) (string, os.Error) {
+	if id == "" {
+		return "", os.NewError("no id specified")
+	}
+	json_buf := url_to_buf(fmt.Sprintf("%s/%s", p.DBURL(), id))
+	id_rev := new(IdAndRev)
+	if err := json.Unmarshal(json_buf, &id_rev); err != nil {
+		return "", err
+	}
+	if id_rev.Id != id {
+		return "", os.NewError("invalid id specified")
+	}
+	return id_rev.Rev, json.Unmarshal(json_buf, d)
+}
+
+// Deletes document given by id and rev.
+func (p Database) Delete(id, rev string) os.Error {
+	headers := map[string][]string{
+		"If-Match": []string{rev},
+	}
+	u := fmt.Sprintf("%s/%s", p.DBURL(), id)
+	ir := response{}
+	if _, err := p.interact("DELETE", u, headers, nil, &ir); err != nil {
+		return err
+	}
+	if !ir.Ok {
+		return os.NewError(fmt.Sprintf("%s: %s", ir.Error, ir.Reason))
+	}
+	return nil
+}
+
+type Row struct {
+	Id  *string
+}
+
+type keyed_view_response struct {
+	Total_rows uint64
+	Offset     uint64
+	Rows       []Row
+}
+
+// Return array of document ids as returned by the given view/options combo.
+// view should be eg. "_design/my_foo/_view/my_bar"
+// options should be eg. { "limit": 10, "key": "baz" }
+func (p Database) QueryIds(view string, options map[string]interface{}) ([]string, os.Error) {
+	kvr := new(keyed_view_response)
+
+	if err := p.Query(view, options, kvr); err != nil {
+		fmt.Println("Query error: " + err.String())
+		return make([]string, 0), err
+	}
+	ids := make([]string, len(kvr.Rows))
+	i := 0
+	for _, row := range kvr.Rows {
+		if row.Id != nil {
+			ids[i] = *row.Id
+			i++
+		}
+	}
+	return ids[:i], nil
+}
+
+func (p Database) Query(view string, options map[string]interface{}, results interface{}) os.Error {
+	if view == "" {
+		return os.NewError("empty view")
+	}
+
+	var parameters string
+	for k, v := range options {
+		switch t := v.(type) {
+		case string:
+			parameters += fmt.Sprintf(`%s=%s&`, k, url.QueryEscape(t))
+		case int:
+			parameters += fmt.Sprintf(`%s=%d&`, k, t)
+		case bool:
+			parameters += fmt.Sprintf(`%s=%v&`, k, t)
+		default:
+			// TODO more types are supported
+			panic(fmt.Sprintf("unsupported value-type %T in Query", t))
+		}
+	}
+	full_url := fmt.Sprintf("%s/%s?%s", p.DBURL(), view, parameters)
+	json_buf := url_to_buf(full_url)
+
+	if err := json.Unmarshal(json_buf, results); err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/tools/music/Makefile b/tools/music/Makefile
new file mode 100644
index 000000000000..488c7eb1b06d
--- /dev/null
+++ b/tools/music/Makefile
@@ -0,0 +1,10 @@
+all: music
+
+music: iTunes.8
+			8l -o music iTunes.8
+
+iTunes.8: iTunes.go
+	8g iTunes.go
+
+clean:
+	rm -rf *.8 music
\ No newline at end of file
diff --git a/tools/music/gettitle b/tools/music/gettitle
new file mode 100755
index 000000000000..0bd4cc6979ec
--- /dev/null
+++ b/tools/music/gettitle
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+osascript -e 'tell application "iTunes" to get name of current track'
+osascript -e 'tell application "iTunes" to get artist of current track'
diff --git a/tools/music/iTunes.8 b/tools/music/iTunes.8
new file mode 100644
index 000000000000..ac5ce58ff133
--- /dev/null
+++ b/tools/music/iTunes.8
Binary files differdiff --git a/tools/music/iTunes.go b/tools/music/iTunes.go
new file mode 100644
index 000000000000..5eb530f6b34b
--- /dev/null
+++ b/tools/music/iTunes.go
@@ -0,0 +1,79 @@
+/* This program is free software. It comes without any warranty, to
+ * the extent permitted by applicable law. You can redistribute it
+ * and/or modify it under the terms of the Do What The Fuck You Want
+ * To Do Public License, Version 3, as published by Vincent Ambo. See
+ * included COPYING file for more details. */
+
+package main
+
+import( "fmt"
+	    "exec"
+		"strings"
+		"http"
+		"url"
+		"flag"
+		"os"
+		"time"
+)
+
+var authkey, host, c_artist, c_title string
+
+func init(){
+	flag.StringVar(&authkey, "key", "none", "http auth key")
+	flag.StringVar(&host, "host", "http://localhost:8080", "host")
+}
+
+func main(){
+	flag.Parse()
+	fmt.Println("Music updater launching. Update occurs once per minute.")
+	go updaterThread()
+
+	var cc string
+	for {
+		fmt.Println("Type \"exit\" to quit")
+		fmt.Scanf("%s", &cc)
+		switch(cc) {
+			case "exit":
+				os.Exit(1)
+			default:
+				fmt.Println("Type \"exit\" to quit")
+
+		}
+	}
+}
+
+func updaterThread(){
+	rValues := make(url.Values)
+	rValues.Add("artist", "")
+	rValues.Add("title", "")
+	rValues.Add("key", authkey)
+
+	for {
+		title, artist := getTrack()
+		if (title != c_title) || (artist != c_artist) {
+			fmt.Println("Updating to: " + title + " - " + artist)
+			c_artist = artist; c_title = title	
+			rValues.Set("artist", artist)
+			rValues.Set("title", title)
+			_, err := http.PostForm(fmt.Sprint(host + "/setsong"), rValues)
+			if err != nil {
+				fmt.Println(err.String())
+			}
+		}
+		time.Sleep(60000000000)
+	}
+}
+
+func getTrack() (title, artist string){
+	a, err := exec.Command("./gettitle").Output()
+	if err != nil {
+		fmt.Println("err: " + err.String())
+		title = ""
+		artist = ""
+	} else {
+		trackInfo := strings.Split(string(a), "\n")
+		title = trackInfo[0]
+		artist = trackInfo[1]
+	}
+	return
+}
\ No newline at end of file
diff --git a/tools/music/music b/tools/music/music
new file mode 100755
index 000000000000..f91b62fe8a9f
--- /dev/null
+++ b/tools/music/music
Binary files differdiff --git a/tools/music/start b/tools/music/start
new file mode 100755
index 000000000000..b9f1358e349f
--- /dev/null
+++ b/tools/music/start
@@ -0,0 +1 @@
+./music -host "http://tazj.in" -key "4058ef41bbca252a7b7e675a61dbf935"