package bouquins import ( "database/sql" "encoding/json" "errors" "fmt" "html/template" "log" "net/http" "net/url" "strconv" "strings" "github.com/c2h5oh/datasize" ) const ( TPL_BOOKS = "book.html" TPL_AUTHORS = "author.html" TPL_SERIES = "series.html" TPL_INDEX = "index.html" PARAM_LIST = "list" PARAM_ORDER = "order" PARAM_SORT = "sort" PARAM_PAGE = "page" PARAM_PERPAGE = "perpage" LIST_AUTHORS = "authors" LIST_SERIES = "series" LIST_BOOKS = "books" URL_INDEX = "/" URL_BOOKS = "/books/" URL_AUTHORS = "/authors/" URL_SERIES = "/series/" URL_JS = "/js/" URL_CSS = "/css/" URL_FONTS = "/fonts/" URL_CALIBRE = "/calibre/" ) type Bouquins struct { *template.Template *sql.DB } /* * A book series. */ type Series struct { Id int64 `json:"id,omitempty"` Name string `json:"name,omitempty"` } /* * A book. Generic data. */ type Book struct { Id int64 `json:"id,omitempty"` Title string `json:"title,omitempty"` SeriesIndex float64 `json:"series_idx,omitempty"` Series *Series `json:"series,omitempty"` } /* * An author. */ type Author struct { Id int64 `json:"id,omitempty"` Name string `json:"name,omitempty"` } /* * Author and number of books. */ type AuthorAdv struct { Author Count int `json:"count,omitempty"` } /* * Downloadable book data. */ type BookData struct { Size int64 `json:"size,omitempty"` Format string `json:"format,omitempty"` Name string `json:"name,omitempty"` } /* * A book. Advanced data: authors, tags. */ type BookAdv struct { Book Authors []*Author `json:"authors,omitempty"` Tags []string `json:"tags,omitempty"` } type AuthorFull struct { Author Books []*Book `json:"books,omitempty"` Series []*Series `json:"series,omitempty"` CoAuthors []*Author `json:"coauthors,omitempty"` } type BookFull struct { BookAdv Data []*BookData `json:"data,omitempty"` Timestamp int64 `json:"timestamp,omitempty"` Pubdate int64 `json:"pubdate,omitempty"` Isbn string `json:"isbn,omitempty"` Lccn string `json:"lccn,omitempty"` Path string `json:"path,omitempty"` Uuid string `json:"uuid,omitempty"` Has_cover bool `json:"has_cover,omitempty"` Lang string `json:"lang,omitempty"` Publisher string `json:"publisher,omitempty"` } type SeriesAdv struct { Series Count int64 `json:"count,omitempty"` Authors []*Author `json:"authors,omitempty"` } type SeriesFull struct { SeriesAdv Books []*Book `json:"books,omitempty"` } type BouquinsModel struct { Title string `json:"title,omitempty"` PageJs string `json:"-"` } // Constructor BouquinsModel func NewBouquinsModel(title string) *BouquinsModel { return NewBouquinsModelJs(title, "") } func NewBouquinsModelJs(title, js string) *BouquinsModel { return &BouquinsModel{ title, js, } } type IndexModel struct { BouquinsModel BooksCount int64 `json:"count"` } // Constructor IndexModel func NewIndexModel(title, js string, count int64) *IndexModel { return &IndexModel{ *NewBouquinsModelJs(title, js), count, } } type ResultsModel struct { Type string `json:"type,omitempty"` More bool `json:"more"` } type BooksResultsModel struct { ResultsModel Results []*BookAdv `json:"results,omitempty"` } func NewBooksResultsModel(books []*BookAdv, more bool) *BooksResultsModel { return &BooksResultsModel{ResultsModel{"books", more}, books} } type AuthorsResultsModel struct { ResultsModel Results []*AuthorAdv `json:"results,omitempty"` } func NewAuthorsResultsModel(authors []*AuthorAdv, more bool) *AuthorsResultsModel { return &AuthorsResultsModel{ResultsModel{"authors", more}, authors} } type SeriesResultsModel struct { ResultsModel Results []*SeriesAdv `json:"results,omitempty"` } func NewSeriesResultsModel(series []*SeriesAdv, more bool) *SeriesResultsModel { return &SeriesResultsModel{ResultsModel{"series", more}, series} } type BookModel struct { BouquinsModel *BookFull } type SeriesModel struct { BouquinsModel *SeriesFull } type AuthorModel struct { BouquinsModel *AuthorFull } func (app *Bouquins) render(res http.ResponseWriter, tpl string, model interface{}) error { return app.Template.ExecuteTemplate(res, tpl, model) } func TemplatesFunc() *template.Template { tpl := template.New("") tpl.Funcs(template.FuncMap{ "humanSize": func(sz int64) string { return datasize.ByteSize(sz).HumanReadable() }, "bookCover": func(book *BookFull) string { fmt.Println(book.Path) return "/calibre/" + url.PathEscape(book.Path) + "/cover.jpg" }, "bookLink": func(data *BookData, book *BookFull) string { return "/calibre/" + url.PathEscape(book.Path) + "/" + url.PathEscape(data.Name) + "." + strings.ToLower(data.Format) }, }) return tpl } func writeJson(res http.ResponseWriter, model interface{}) error { res.Header().Set("Content-Type", "application/json") enc := json.NewEncoder(res) return enc.Encode(model) } func isJson(req *http.Request) bool { return req.Header.Get("Accept") == "application/json" } func paramInt(name string, req *http.Request) int { val := req.URL.Query().Get(name) valInt, err := strconv.Atoi(val) if err != nil { log.Println("Invalid value for", name, ":", val) return 0 } return valInt } func paramOrder(req *http.Request) string { val := req.URL.Query().Get(PARAM_ORDER) if val == "desc" || val == "asc" { return val } return "" } // return limit, offset, sort, order func paramPaginate(req *http.Request) (int, int, string, string) { page, perpage := paramInt(PARAM_PAGE, req), paramInt(PARAM_PERPAGE, req) limit := perpage if perpage == 0 { limit = DEF_LIM } offset := perpage * (page - 1) if offset < 0 { offset = 0 } sort := req.URL.Query().Get(PARAM_SORT) order := paramOrder(req) return limit, offset, sort, order } // ROUTES // func (app *Bouquins) IndexPage(res http.ResponseWriter, req *http.Request) { count, err := app.BookCount() if err != nil { log.Print(err) } model := NewIndexModel("", "index.js", count) if isJson(req) { err := writeJson(res, model) if err != nil { log.Println(err) http.Error(res, err.Error(), 500) } } else { app.render(res, TPL_INDEX, model) } } func (app *Bouquins) BooksListPage(res http.ResponseWriter, req *http.Request) error { if isJson(req) { books, more, err := app.BooksAdv(paramPaginate(req)) if err != nil { return err } return writeJson(res, NewBooksResultsModel(books, more)) } return errors.New("Invalid mime") } func (app *Bouquins) BookPage(idParam string, res http.ResponseWriter, req *http.Request) error { id, err := strconv.Atoi(idParam) if err != nil { return nil } book, err := app.BookFull(int64(id)) if err != nil { return nil } return app.render(res, TPL_BOOKS, &BookModel{*NewBouquinsModel(book.Title), book}) } func (app *Bouquins) BooksPage(res http.ResponseWriter, req *http.Request) { var err error var idParam = "" if strings.HasPrefix(req.URL.Path, URL_BOOKS) { idParam = req.URL.Path[len(URL_BOOKS):] } else { err = errors.New("Invalid URL") // FIXME 404 } if len(idParam) == 0 { err = app.BooksListPage(res, req) } else { err = app.BookPage(idParam, res, req) } if err != nil { log.Println(err) http.Error(res, err.Error(), 500) } } func (app *Bouquins) AuthorsListPage(res http.ResponseWriter, req *http.Request) error { if isJson(req) { authors, more, err := app.AuthorsAdv(paramPaginate(req)) if err != nil { return err } return writeJson(res, NewAuthorsResultsModel(authors, more)) } return errors.New("Invalid mime") } func (app *Bouquins) AuthorPage(idParam string, res http.ResponseWriter, req *http.Request) error { id, err := strconv.Atoi(idParam) if err != nil { return err } author, err := app.AuthorFull(int64(id)) if err != nil { return err } return app.render(res, TPL_AUTHORS, &AuthorModel{*NewBouquinsModelJs(author.Name, "author.js"), author}) } func (app *Bouquins) AuthorsPage(res http.ResponseWriter, req *http.Request) { var err error var idParam = "" if strings.HasPrefix(req.URL.Path, URL_AUTHORS) { idParam = req.URL.Path[len(URL_AUTHORS):] } else { err = errors.New("Invalid URL") // FIXME 404 } if len(idParam) == 0 { err = app.AuthorsListPage(res, req) } else { err = app.AuthorPage(idParam, res, req) } if err != nil { log.Println(err) http.Error(res, err.Error(), 500) } } func (app *Bouquins) SeriesListPage(res http.ResponseWriter, req *http.Request) error { if isJson(req) { series, more, err := app.SeriesAdv(paramPaginate(req)) if err != nil { return err } return writeJson(res, NewSeriesResultsModel(series, more)) } return errors.New("Invalid mime") } func (app *Bouquins) SeriePage(idParam string, res http.ResponseWriter, req *http.Request) error { id, err := strconv.Atoi(idParam) if err != nil { return err } series, err := app.SeriesFull(int64(id)) if err != nil { return err } return app.render(res, TPL_SERIES, &SeriesModel{*NewBouquinsModel(series.Name), series}) } func (app *Bouquins) SeriesPage(res http.ResponseWriter, req *http.Request) { var err error var idParam = "" if strings.HasPrefix(req.URL.Path, URL_SERIES) { idParam = req.URL.Path[len(URL_SERIES):] } else { err = errors.New("Invalid URL") // FIXME 404 } if len(idParam) == 0 { err = app.SeriesListPage(res, req) } else { err = app.SeriePage(idParam, res, req) } if err != nil { log.Println(err) http.Error(res, err.Error(), 500) } }