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" TPL_SEARCH = "search.html" PARAM_LIST = "list" PARAM_ORDER = "order" PARAM_SORT = "sort" PARAM_PAGE = "page" PARAM_PERPAGE = "perpage" PARAM_TERM = "term" LIST_AUTHORS = "authors" LIST_SERIES = "series" LIST_BOOKS = "books" URL_INDEX = "/" URL_BOOKS = "/books/" URL_AUTHORS = "/authors/" URL_SERIES = "/series/" URL_SEARCH = "/search/" 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"` } // Constructor BouquinsModel func NewBouquinsModel(title string) *BouquinsModel { return &BouquinsModel{title} } type IndexModel struct { BouquinsModel BooksCount int64 `json:"count"` } // Constructor IndexModel func NewIndexModel(title string, count int64) *IndexModel { return &IndexModel{*NewBouquinsModel(title), count} } type SearchModel struct { BouquinsModel } func NewSearchModel() *SearchModel { return &SearchModel{*NewBouquinsModel("Recherche")} } type ResultsModel struct { Type string `json:"type,omitempty"` More bool `json:"more"` CountResults int `json:"count,omitempty"` } type BooksResultsModel struct { ResultsModel Results []*BookAdv `json:"results,omitempty"` } func NewBooksResultsModel(books []*BookAdv, more bool, count int) *BooksResultsModel { return &BooksResultsModel{ResultsModel{"books", more, count}, books} } type AuthorsResultsModel struct { ResultsModel Results []*AuthorAdv `json:"results,omitempty"` } func NewAuthorsResultsModel(authors []*AuthorAdv, more bool, count int) *AuthorsResultsModel { return &AuthorsResultsModel{ResultsModel{"authors", more, count}, authors} } type SeriesResultsModel struct { ResultsModel Results []*SeriesAdv `json:"results,omitempty"` } func NewSeriesResultsModel(series []*SeriesAdv, more bool, count int) *SeriesResultsModel { return &SeriesResultsModel{ResultsModel{"series", more, count}, series} } type BookModel struct { BouquinsModel *BookFull } type SeriesModel struct { BouquinsModel *SeriesFull } type AuthorModel struct { BouquinsModel *AuthorFull } type ReqParams struct { Limit int Offset int Sort string Order string Terms []string AllWords bool } 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) if val == "" { return 0 } 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 "" } func params(req *http.Request) *ReqParams { 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) terms := req.URL.Query()[PARAM_TERM] return &ReqParams{limit, offset, sort, order, terms, false} } // ROUTES // func (app *Bouquins) IndexPage(res http.ResponseWriter, req *http.Request) { count, err := app.BookCount() if err != nil { log.Print(err) } model := NewIndexModel("", count) if isJson(req) { err := writeJson(res, model) if err != nil { log.Println(err) http.Error(res, err.Error(), 500) } } else { err = app.render(res, TPL_INDEX, model) if err != nil { log.Println(err) } } } func (app *Bouquins) BooksListPage(res http.ResponseWriter, req *http.Request) error { if isJson(req) { books, count, more, err := app.BooksAdv(params(req)) if err != nil { return err } return writeJson(res, NewBooksResultsModel(books, more, count)) } 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, count, more, err := app.AuthorsAdv(params(req)) if err != nil { return err } return writeJson(res, NewAuthorsResultsModel(authors, more, count)) } 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{*NewBouquinsModel(author.Name), 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, count, more, err := app.SeriesAdv(params(req)) if err != nil { return err } return writeJson(res, NewSeriesResultsModel(series, more, count)) } 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) } } func (app *Bouquins) SearchPage(res http.ResponseWriter, req *http.Request) { model := NewSearchModel() err := app.render(res, TPL_SEARCH, model) if err != nil { log.Println(err) } }