package main import ( "fmt" "http" "exp/template" "os" "json" "strconv" "time" "rand" "unicode" "utf8" ) type Song struct { Yid string Title string User string } type Playlist struct { Id string } var templates *template.Set const debug = false func main() { templates = template.SetMust(template.ParseTemplateFiles("templates/*.html")) rand.Seed(time.Nanoseconds()) initDb() http.HandleFunc("/", home) http.HandleFunc("/p/", playlist) http.HandleFunc("/add/", add) http.HandleFunc("/remove/", remove) http.HandleFunc("/move/", move) http.HandleFunc("/poll/", poll) http.HandleFunc("/create/", create) err := http.ListenAndServe("localhost:8000", nil) if err != nil { fmt.Println(err) os.Exit(1) } } func renderPage(w http.ResponseWriter, page string, data interface{}) { if debug { var err os.Error templates, err = template.ParseTemplateFiles("templates/*.html") if err != nil { fmt.Fprintln(os.Stderr, err.String()) http.Error(w, err.String(), http.StatusInternalServerError) return } } err := templates.Execute(w, page + ".html", data) if err != nil { fmt.Fprintln(os.Stderr, err.String()) } } func home(w http.ResponseWriter, r *http.Request) { renderPage(w, "home", nil) go track("home", r.Header.Get("X-Forwarded-For"), "", "", "") } func playlist(w http.ResponseWriter, r *http.Request) { id := r.URL.Path[len("/p/"):] if len(id) < 8 { http.Redirect(w, r, "/", http.StatusSeeOther) return } db := <-dbPool defer func () {dbPool <- db}() count, err := queryInt(db, "SELECT COUNT(`pid`) FROM `playlist` WHERE `id` = ?", id) if count == 0 || err != nil { http.Redirect(w, r, "/", http.StatusSeeOther) return } p := Playlist{Id: id} renderPage(w, "p", p) go track("p", r.Header.Get("X-Forwarded-For"), id, "", "") } func add(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() db := <-dbPool defer func () {dbPool <- db}() pid := getpid(db, q.Get("pid")) if pid == -1 { http.Error(w, "invalid pid", http.StatusBadRequest) return } err := db.Start() if err != nil { http.Error(w, err.String(), http.StatusInternalServerError) return } // unfortunately, MAX(`order`) returns NULL when there is nothing // so query for COUNT(`sid`) count, err := queryInt(db, "SELECT COUNT(`sid`) FROM `song` WHERE `pid` = ?", pid) if err != nil { db.Rollback() http.Error(w, err.String(), http.StatusInternalServerError) return } _, err = prepare(db, "INSERT INTO `song` (`pid`,`yid`,`title`,`user`,`order`) VALUES(?, ?, ?, ?, ?)", pid, q.Get("yid"), q.Get("title"), q.Get("user"), count) if err != nil { db.Rollback() http.Error(w, err.String(), http.StatusInternalServerError) return } err = db.Commit() if err != nil { http.Error(w, err.String(), http.StatusInternalServerError) return } w.Write([]byte("1")) addUpdate(pid, addAction, &Song{Yid: q.Get("yid"), Title: q.Get("title"), User: q.Get("user")}) go track("add", r.Header.Get("X-Forwarded-For"), q.Get("pid"), q.Get("title"), q.Get("user")) } func remove(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() db := <-dbPool defer func () {dbPool <- db}() pid := getpid(db, q.Get("pid")) if pid == -1 { http.Error(w, "invalid pid", http.StatusBadRequest) return } err := db.Start() if err != nil { http.Error(w, err.String(), http.StatusInternalServerError) return } order, err := queryInt(db, "SELECT `order` FROM `song` WHERE `yid` = ? AND `pid` = ?", q.Get("yid"), pid) if err != nil { db.Rollback() http.Error(w, err.String(), http.StatusInternalServerError) return } _, err = prepare(db, "DELETE FROM `song` WHERE `pid` = ? AND yid = ?", pid, q.Get("yid")) if err != nil { db.Rollback() http.Error(w, err.String(), http.StatusInternalServerError) return } _, err = prepare(db, "UPDATE `song` SET `order` = `order`-1 WHERE `order` > ? AND `pid` = ?", order, pid) if err != nil { db.Rollback() http.Error(w, err.String(), http.StatusInternalServerError) return } err = db.Commit() if err != nil { http.Error(w, err.String(), http.StatusInternalServerError) return } w.Write([]byte("1")) addUpdate(pid, removeAction, &Song{Yid: q.Get("yid")}) go track("remove", r.Header.Get("X-Forwarded-For"), q.Get("pid"), "", "") } func move(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() db := <-dbPool defer func () {dbPool <- db}() pid := getpid(db, q.Get("pid")) if pid == -1 { http.Error(w, "invalid pid", http.StatusBadRequest) return } direction, err := strconv.Atoui(q.Get("direction")) if err != nil { http.Error(w, err.String(), http.StatusInternalServerError) return } err = db.Start() if err != nil { http.Error(w, err.String(), http.StatusInternalServerError) return } order, err := queryInt(db, "SELECT `order` FROM `song` WHERE `yid` = ? AND `pid` = ?", q.Get("yid"), pid) if err != nil { db.Rollback() http.Error(w, err.String(), http.StatusInternalServerError) return } newOrder := order if direction == moveUpAction && order > 0 { newOrder-- } else if direction == moveDownAction { newOrder++ } else { db.Rollback() http.Error(w, "invalid direction or cannot move up", http.StatusBadRequest) return } query, err := prepare(db, "UPDATE `song` SET `order` = ? WHERE `order` = ? AND `pid` = ?", order, newOrder, pid) if err != nil { db.Rollback() http.Error(w, err.String(), http.StatusInternalServerError) return } else if query.AffectedRows != 1 { db.Rollback() http.Error(w, "invalid direction for this song", http.StatusBadRequest) return } // there are now two songs with that order, so also check yid _, err = prepare(db, "UPDATE `song` SET `order` = ? WHERE `order` = ? AND `pid` = ? AND `yid` = ?", newOrder, order, pid, q.Get("yid")) if err != nil { db.Rollback() http.Error(w, err.String(), http.StatusInternalServerError) return } err = db.Commit() if err != nil { http.Error(w, err.String(), http.StatusInternalServerError) return } w.Write([]byte("1")) addUpdate(pid, direction, &Song{Yid: q.Get("yid")}) } func poll(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() timestamp := q.Get("timestamp") if timestamp == "-1" { db := <-dbPool defer func () {dbPool <- db}() query, err := prepare(db, "SELECT `yid`,`title`,`user` FROM `playlist` JOIN `song` USING(`pid`) WHERE `id` = ? ORDER BY `order` ASC", q.Get("pid")) updates := make([]Update, 0, 2) for { song := new(Song) query.BindResult(&song.Yid, &song.Title, &song.User) eof, err := query.Fetch() if err != nil { http.Error(w, err.String(), http.StatusInternalServerError) return } if eof { break } updates = append(updates, Update{Song: song, Action: addAction, Timestamp: time.Nanoseconds()}) } err = query.FreeResult() if err != nil { http.Error(w, err.String(), http.StatusInternalServerError) return } output, err := json.MarshalForHTML(updates) if err != nil { http.Error(w, err.String(), http.StatusInternalServerError) return } w.Write(output) } else { timestamp, err := strconv.Atoi64(q.Get("timestamp")) if err != nil { http.Error(w, err.String(), http.StatusInternalServerError) return } update := getUpdates(q.Get("pid"), timestamp) if update != nil { w.Write([]byte("[")) for update != nil { output, err := json.MarshalForHTML(update) if err == nil { w.Write(output) } update = update.Next if update != nil { w.Write([]byte(",")) } } w.Write([]byte("]")) return } w.Write([]byte("[]")) } } func create(w http.ResponseWriter, r *http.Request) { id := make([]byte, 24) pos := id for i := 0; i < 8; i++ { for { rune := rand.Intn(65536) // mysql only supports the first 65535 code points if unicode.IsGraphic(rune) { bytes := utf8.EncodeRune(pos, rune) pos = pos[bytes:] break } } } idStr := string(id) db := <-dbPool defer func () {dbPool <- db}() _, err := prepare(db, "INSERT INTO `playlist` (`id`) VALUES(?)", idStr) if err != nil { http.Error(w, err.String(), http.StatusInternalServerError) return } http.Redirect(w, r, "/p/" + idStr, http.StatusSeeOther) go track("create",r.Header.Get("X-Forwarded-For"), idStr, "", "") }