2017-07-30 14:06:38 +00:00
|
|
|
package bouquins
|
|
|
|
|
2017-07-30 15:20:48 +00:00
|
|
|
import (
|
2017-07-30 18:14:20 +00:00
|
|
|
"database/sql"
|
2017-07-31 18:49:27 +00:00
|
|
|
"encoding/json"
|
2017-08-05 15:48:06 +00:00
|
|
|
"errors"
|
2017-08-04 18:06:10 +00:00
|
|
|
"fmt"
|
2017-07-30 15:20:48 +00:00
|
|
|
"html/template"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
2017-08-04 17:47:15 +00:00
|
|
|
"net/url"
|
2017-08-01 18:00:20 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2017-08-04 18:06:10 +00:00
|
|
|
|
|
|
|
"github.com/c2h5oh/datasize"
|
2017-07-30 15:20:48 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2017-07-30 18:14:20 +00:00
|
|
|
TPL_BOOKS = "book.html"
|
|
|
|
TPL_AUTHORS = "author.html"
|
2017-07-30 15:20:48 +00:00
|
|
|
TPL_SERIES = "series.html"
|
|
|
|
TPL_INDEX = "index.html"
|
2017-07-31 18:14:57 +00:00
|
|
|
|
2017-08-05 17:40:58 +00:00
|
|
|
PARAM_LIST = "list"
|
|
|
|
PARAM_ORDER = "order"
|
|
|
|
PARAM_SORT = "sort"
|
|
|
|
PARAM_PAGE = "page"
|
|
|
|
PARAM_PERPAGE = "perpage"
|
2017-07-31 18:14:57 +00:00
|
|
|
|
|
|
|
LIST_AUTHORS = "authors"
|
|
|
|
LIST_SERIES = "series"
|
|
|
|
LIST_BOOKS = "books"
|
2017-08-01 18:00:20 +00:00
|
|
|
|
|
|
|
URL_INDEX = "/"
|
|
|
|
URL_BOOKS = "/books/"
|
|
|
|
URL_AUTHORS = "/authors/"
|
|
|
|
URL_SERIES = "/series/"
|
|
|
|
URL_JS = "/js/"
|
|
|
|
URL_CSS = "/css/"
|
|
|
|
URL_FONTS = "/fonts/"
|
2017-08-04 18:06:10 +00:00
|
|
|
URL_CALIBRE = "/calibre/"
|
2017-07-30 15:20:48 +00:00
|
|
|
)
|
2017-07-30 14:06:38 +00:00
|
|
|
|
|
|
|
type Bouquins struct {
|
2017-07-30 15:20:48 +00:00
|
|
|
*template.Template
|
2017-07-30 18:14:20 +00:00
|
|
|
*sql.DB
|
2017-07-30 14:06:38 +00:00
|
|
|
}
|
|
|
|
|
2017-07-30 14:42:18 +00:00
|
|
|
/*
|
|
|
|
* A book series.
|
|
|
|
*/
|
|
|
|
type Series struct {
|
2017-07-31 18:49:27 +00:00
|
|
|
Id int64 `json:"id,omitempty"`
|
|
|
|
Name string `json:"name,omitempty"`
|
2017-07-30 14:42:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* A book. Generic data.
|
|
|
|
*/
|
|
|
|
type Book struct {
|
2017-07-31 18:49:27 +00:00
|
|
|
Id int64 `json:"id,omitempty"`
|
|
|
|
Title string `json:"title,omitempty"`
|
2017-08-01 18:00:20 +00:00
|
|
|
SeriesIndex float64 `json:"series_idx,omitempty"`
|
2017-07-31 18:49:27 +00:00
|
|
|
Series *Series `json:"series,omitempty"`
|
2017-07-30 14:42:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* An author.
|
|
|
|
*/
|
|
|
|
type Author struct {
|
2017-07-31 18:49:27 +00:00
|
|
|
Id int64 `json:"id,omitempty"`
|
|
|
|
Name string `json:"name,omitempty"`
|
2017-07-30 14:42:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Author and number of books.
|
|
|
|
*/
|
|
|
|
type AuthorAdv struct {
|
|
|
|
Author
|
2017-07-31 18:49:27 +00:00
|
|
|
Count int `json:"count,omitempty"`
|
2017-07-30 14:42:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Downloadable book data.
|
|
|
|
*/
|
|
|
|
type BookData struct {
|
2017-07-31 18:49:27 +00:00
|
|
|
Size int64 `json:"size,omitempty"`
|
|
|
|
Format string `json:"format,omitempty"`
|
|
|
|
Name string `json:"name,omitempty"`
|
2017-07-30 14:42:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* A book. Advanced data: authors, tags.
|
|
|
|
*/
|
|
|
|
type BookAdv struct {
|
|
|
|
Book
|
2017-07-31 18:49:27 +00:00
|
|
|
Authors []*Author `json:"authors,omitempty"`
|
|
|
|
Tags []string `json:"tags,omitempty"`
|
2017-07-30 14:42:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type AuthorFull struct {
|
|
|
|
Author
|
2017-08-04 16:57:15 +00:00
|
|
|
Books []*Book `json:"books,omitempty"`
|
|
|
|
Series []*Series `json:"series,omitempty"`
|
|
|
|
CoAuthors []*Author `json:"coauthors,omitempty"`
|
2017-07-30 14:42:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type BookFull struct {
|
|
|
|
BookAdv
|
2017-08-01 18:00:20 +00:00
|
|
|
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"`
|
2017-07-30 14:42:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type SeriesAdv struct {
|
|
|
|
Series
|
2017-07-31 18:49:27 +00:00
|
|
|
Count int64 `json:"count,omitempty"`
|
|
|
|
Authors []*Author `json:"authors,omitempty"`
|
2017-07-30 14:42:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type SeriesFull struct {
|
|
|
|
SeriesAdv
|
2017-07-31 18:49:27 +00:00
|
|
|
Books []*Book `json:"books,omitempty"`
|
2017-07-30 15:20:48 +00:00
|
|
|
}
|
|
|
|
|
2017-07-30 16:09:27 +00:00
|
|
|
type BouquinsModel struct {
|
2017-08-04 16:57:15 +00:00
|
|
|
Title string `json:"title,omitempty"`
|
|
|
|
PageJs string `json:"-"`
|
2017-07-30 16:09:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Constructor BouquinsModel
|
|
|
|
func NewBouquinsModel(title string) *BouquinsModel {
|
2017-08-04 16:57:15 +00:00
|
|
|
return NewBouquinsModelJs(title, "")
|
|
|
|
}
|
|
|
|
func NewBouquinsModelJs(title, js string) *BouquinsModel {
|
2017-07-30 16:09:27 +00:00
|
|
|
return &BouquinsModel{
|
|
|
|
title,
|
2017-08-04 16:57:15 +00:00
|
|
|
js,
|
2017-07-30 16:09:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-30 15:20:48 +00:00
|
|
|
type IndexModel struct {
|
2017-07-30 16:09:27 +00:00
|
|
|
BouquinsModel
|
2017-08-05 14:44:05 +00:00
|
|
|
BooksCount int64 `json:"count"`
|
2017-07-30 14:42:18 +00:00
|
|
|
}
|
|
|
|
|
2017-07-30 16:09:27 +00:00
|
|
|
// Constructor IndexModel
|
2017-08-05 09:16:19 +00:00
|
|
|
func NewIndexModel(title, js string, count int64) *IndexModel {
|
2017-07-30 16:09:27 +00:00
|
|
|
return &IndexModel{
|
2017-08-05 09:16:19 +00:00
|
|
|
*NewBouquinsModelJs(title, js),
|
2017-07-30 16:09:27 +00:00
|
|
|
count,
|
2017-07-30 15:20:48 +00:00
|
|
|
}
|
2017-07-30 16:09:27 +00:00
|
|
|
}
|
|
|
|
|
2017-08-05 15:48:06 +00:00
|
|
|
type ResultsModel struct {
|
|
|
|
Type string `json:"type,omitempty"`
|
2017-08-05 17:40:58 +00:00
|
|
|
More bool `json:"more"`
|
2017-08-05 15:48:06 +00:00
|
|
|
}
|
2017-08-05 14:44:05 +00:00
|
|
|
type BooksResultsModel struct {
|
2017-08-05 15:48:06 +00:00
|
|
|
ResultsModel
|
2017-08-05 14:44:05 +00:00
|
|
|
Results []*BookAdv `json:"results,omitempty"`
|
|
|
|
}
|
|
|
|
|
2017-08-05 17:40:58 +00:00
|
|
|
func NewBooksResultsModel(books []*BookAdv, more bool) *BooksResultsModel {
|
|
|
|
return &BooksResultsModel{ResultsModel{"books", more}, books}
|
2017-08-05 15:48:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type AuthorsResultsModel struct {
|
|
|
|
ResultsModel
|
|
|
|
Results []*AuthorAdv `json:"results,omitempty"`
|
|
|
|
}
|
|
|
|
|
2017-08-05 17:40:58 +00:00
|
|
|
func NewAuthorsResultsModel(authors []*AuthorAdv, more bool) *AuthorsResultsModel {
|
|
|
|
return &AuthorsResultsModel{ResultsModel{"authors", more}, authors}
|
2017-08-05 15:48:06 +00:00
|
|
|
}
|
|
|
|
|
2017-08-05 16:24:57 +00:00
|
|
|
type SeriesResultsModel struct {
|
2017-08-05 15:48:06 +00:00
|
|
|
ResultsModel
|
|
|
|
Results []*SeriesAdv `json:"results,omitempty"`
|
2017-08-05 14:44:05 +00:00
|
|
|
}
|
|
|
|
|
2017-08-05 17:40:58 +00:00
|
|
|
func NewSeriesResultsModel(series []*SeriesAdv, more bool) *SeriesResultsModel {
|
|
|
|
return &SeriesResultsModel{ResultsModel{"series", more}, series}
|
2017-08-05 15:48:06 +00:00
|
|
|
}
|
2017-08-05 14:44:05 +00:00
|
|
|
|
2017-08-01 13:38:23 +00:00
|
|
|
type BookModel struct {
|
|
|
|
BouquinsModel
|
|
|
|
*BookFull
|
|
|
|
}
|
|
|
|
|
2017-08-03 17:51:56 +00:00
|
|
|
type SeriesModel struct {
|
|
|
|
BouquinsModel
|
|
|
|
*SeriesFull
|
|
|
|
}
|
|
|
|
|
2017-08-03 18:36:46 +00:00
|
|
|
type AuthorModel struct {
|
|
|
|
BouquinsModel
|
|
|
|
*AuthorFull
|
|
|
|
}
|
|
|
|
|
2017-08-05 15:48:06 +00:00
|
|
|
func (app *Bouquins) render(res http.ResponseWriter, tpl string, model interface{}) error {
|
|
|
|
return app.Template.ExecuteTemplate(res, tpl, model)
|
2017-07-30 14:06:38 +00:00
|
|
|
}
|
2017-08-04 17:47:15 +00:00
|
|
|
func TemplatesFunc() *template.Template {
|
|
|
|
tpl := template.New("")
|
|
|
|
tpl.Funcs(template.FuncMap{
|
|
|
|
"humanSize": func(sz int64) string {
|
|
|
|
return datasize.ByteSize(sz).HumanReadable()
|
|
|
|
},
|
2017-08-04 18:06:10 +00:00
|
|
|
"bookCover": func(book *BookFull) string {
|
|
|
|
fmt.Println(book.Path)
|
|
|
|
return "/calibre/" + url.PathEscape(book.Path) + "/cover.jpg"
|
|
|
|
},
|
2017-08-04 17:47:15 +00:00
|
|
|
"bookLink": func(data *BookData, book *BookFull) string {
|
2017-08-04 18:06:10 +00:00
|
|
|
return "/calibre/" + url.PathEscape(book.Path) + "/" + url.PathEscape(data.Name) + "." + strings.ToLower(data.Format)
|
2017-08-04 17:47:15 +00:00
|
|
|
},
|
|
|
|
})
|
|
|
|
return tpl
|
|
|
|
}
|
2017-07-30 16:09:27 +00:00
|
|
|
|
2017-08-05 15:48:06 +00:00
|
|
|
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"
|
|
|
|
}
|
2017-08-02 18:17:12 +00:00
|
|
|
func paramInt(name string, req *http.Request) int {
|
|
|
|
val := req.URL.Query().Get(name)
|
|
|
|
valInt, err := strconv.Atoi(val)
|
|
|
|
if err != nil {
|
2017-08-05 09:16:19 +00:00
|
|
|
log.Println("Invalid value for", name, ":", val)
|
2017-08-02 18:17:12 +00:00
|
|
|
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 ""
|
|
|
|
}
|
|
|
|
|
2017-08-05 17:40:58 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2017-07-30 18:14:20 +00:00
|
|
|
// ROUTES //
|
|
|
|
|
2017-07-30 16:09:27 +00:00
|
|
|
func (app *Bouquins) IndexPage(res http.ResponseWriter, req *http.Request) {
|
2017-07-30 18:14:20 +00:00
|
|
|
count, err := app.BookCount()
|
|
|
|
if err != nil {
|
|
|
|
log.Print(err)
|
|
|
|
}
|
2017-08-05 09:16:19 +00:00
|
|
|
model := NewIndexModel("", "index.js", count)
|
2017-08-05 15:48:06 +00:00
|
|
|
if isJson(req) {
|
|
|
|
err := writeJson(res, model)
|
2017-07-31 18:49:27 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
http.Error(res, err.Error(), 500)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
app.render(res, TPL_INDEX, model)
|
|
|
|
}
|
2017-07-30 16:09:27 +00:00
|
|
|
}
|
2017-08-05 15:48:06 +00:00
|
|
|
func (app *Bouquins) BooksListPage(res http.ResponseWriter, req *http.Request) error {
|
|
|
|
if isJson(req) {
|
2017-08-05 17:40:58 +00:00
|
|
|
books, more, err := app.BooksAdv(paramPaginate(req))
|
2017-08-05 15:48:06 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-08-05 17:40:58 +00:00
|
|
|
return writeJson(res, NewBooksResultsModel(books, more))
|
2017-08-05 15:48:06 +00:00
|
|
|
}
|
|
|
|
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})
|
|
|
|
}
|
2017-07-30 15:20:48 +00:00
|
|
|
func (app *Bouquins) BooksPage(res http.ResponseWriter, req *http.Request) {
|
2017-08-05 15:48:06 +00:00
|
|
|
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
|
2017-08-01 18:00:20 +00:00
|
|
|
}
|
2017-08-05 09:16:19 +00:00
|
|
|
if len(idParam) == 0 {
|
2017-08-05 15:48:06 +00:00
|
|
|
err = app.BooksListPage(res, req)
|
2017-08-05 09:16:19 +00:00
|
|
|
} else {
|
2017-08-05 15:48:06 +00:00
|
|
|
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) {
|
2017-08-05 17:40:58 +00:00
|
|
|
authors, more, err := app.AuthorsAdv(paramPaginate(req))
|
2017-08-05 09:16:19 +00:00
|
|
|
if err != nil {
|
2017-08-05 15:48:06 +00:00
|
|
|
return err
|
2017-08-05 09:16:19 +00:00
|
|
|
}
|
2017-08-05 17:40:58 +00:00
|
|
|
return writeJson(res, NewAuthorsResultsModel(authors, more))
|
2017-08-01 13:38:23 +00:00
|
|
|
}
|
2017-08-05 15:48:06 +00:00
|
|
|
return errors.New("Invalid mime")
|
2017-07-30 14:06:38 +00:00
|
|
|
}
|
2017-08-05 15:48:06 +00:00
|
|
|
func (app *Bouquins) AuthorPage(idParam string, res http.ResponseWriter, req *http.Request) error {
|
|
|
|
id, err := strconv.Atoi(idParam)
|
2017-08-03 18:36:46 +00:00
|
|
|
if err != nil {
|
2017-08-05 15:48:06 +00:00
|
|
|
return err
|
2017-08-03 18:36:46 +00:00
|
|
|
}
|
|
|
|
author, err := app.AuthorFull(int64(id))
|
|
|
|
if err != nil {
|
2017-08-05 15:48:06 +00:00
|
|
|
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)
|
2017-08-03 18:36:46 +00:00
|
|
|
}
|
2017-07-30 14:06:38 +00:00
|
|
|
}
|
2017-08-05 16:24:57 +00:00
|
|
|
func (app *Bouquins) SeriesListPage(res http.ResponseWriter, req *http.Request) error {
|
|
|
|
if isJson(req) {
|
2017-08-05 17:40:58 +00:00
|
|
|
series, more, err := app.SeriesAdv(paramPaginate(req))
|
2017-08-05 16:24:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-08-05 17:40:58 +00:00
|
|
|
return writeJson(res, NewSeriesResultsModel(series, more))
|
2017-08-03 17:51:56 +00:00
|
|
|
}
|
2017-08-05 16:24:57 +00:00
|
|
|
return errors.New("Invalid mime")
|
|
|
|
}
|
|
|
|
func (app *Bouquins) SeriePage(idParam string, res http.ResponseWriter, req *http.Request) error {
|
|
|
|
id, err := strconv.Atoi(idParam)
|
2017-08-03 17:51:56 +00:00
|
|
|
if err != nil {
|
2017-08-05 16:24:57 +00:00
|
|
|
return err
|
2017-08-03 17:51:56 +00:00
|
|
|
}
|
|
|
|
series, err := app.SeriesFull(int64(id))
|
2017-08-03 18:36:46 +00:00
|
|
|
if err != nil {
|
2017-08-05 16:24:57 +00:00
|
|
|
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)
|
2017-08-03 18:36:46 +00:00
|
|
|
}
|
2017-07-30 14:06:38 +00:00
|
|
|
}
|