[add] editing, deletion, metadata recording
This commit is contained in:
parent
3150c89303
commit
64ab10c95e
@ -25,7 +25,7 @@ type API struct {
|
||||
|
||||
func NewApi(db *database.DBManager, c *config.Config) *API {
|
||||
api := &API{
|
||||
HTMLPolicy: bluemonday.StripTagsPolicy(),
|
||||
HTMLPolicy: bluemonday.StrictPolicy(),
|
||||
Router: gin.Default(),
|
||||
Config: c,
|
||||
DB: db,
|
||||
@ -97,6 +97,9 @@ func (api *API) registerWebAppRoutes() {
|
||||
api.Router.GET("/activity", api.authWebAppMiddleware, api.createAppResourcesRoute("activity"))
|
||||
api.Router.GET("/documents", api.authWebAppMiddleware, api.createAppResourcesRoute("documents"))
|
||||
api.Router.GET("/documents/:document", api.authWebAppMiddleware, api.createAppResourcesRoute("document"))
|
||||
api.Router.DELETE("/documents/:document", api.authWebAppMiddleware, api.deleteDocument)
|
||||
api.Router.POST("/documents/:document/edit", api.authWebAppMiddleware, api.createAppResourcesRoute("document-edit"))
|
||||
api.Router.POST("/documents/:document/delete", api.authWebAppMiddleware, api.deleteDocument)
|
||||
api.Router.GET("/documents/:document/file", api.authWebAppMiddleware, api.downloadDocumentFile)
|
||||
api.Router.GET("/documents/:document/cover", api.authWebAppMiddleware, api.getDocumentCover)
|
||||
|
||||
|
@ -2,17 +2,29 @@ package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/slices"
|
||||
"reichard.io/bbank/database"
|
||||
"reichard.io/bbank/metadata"
|
||||
)
|
||||
|
||||
type requestDocumentEdit struct {
|
||||
Title *string `form:"title"`
|
||||
Author *string `form:"author"`
|
||||
Description *string `form:"description"`
|
||||
RemoveCover *string `form:"remove_cover"`
|
||||
CoverFile *multipart.FileHeader `form:"cover"`
|
||||
}
|
||||
|
||||
func baseResourceRoute(template string, args ...map[string]any) func(c *gin.Context) {
|
||||
variables := gin.H{"RouteName": template}
|
||||
if len(args) > 0 {
|
||||
@ -84,6 +96,92 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
|
||||
}
|
||||
|
||||
templateVars["Data"] = document
|
||||
} else if routeName == "document-edit" {
|
||||
var rDocID requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDocID); err != nil {
|
||||
log.Error("[createAppResourcesRoute] Invalid URI Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
var rDocEdit requestDocumentEdit
|
||||
if err := c.ShouldBind(&rDocEdit); err != nil {
|
||||
log.Error("[createAppResourcesRoute] Invalid Form Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate Something Exists
|
||||
if rDocEdit.Author == nil &&
|
||||
rDocEdit.Title == nil &&
|
||||
rDocEdit.Description == nil &&
|
||||
rDocEdit.CoverFile == nil &&
|
||||
rDocEdit.RemoveCover == nil {
|
||||
log.Error("[createAppResourcesRoute] Missing Form Values")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
// Handle Cover
|
||||
var coverFileName *string
|
||||
if rDocEdit.RemoveCover != nil && *rDocEdit.RemoveCover == "on" {
|
||||
s := "UNKNOWN"
|
||||
coverFileName = &s
|
||||
} else if rDocEdit.CoverFile != nil {
|
||||
|
||||
// Validate Type & Derive Extension on MIME
|
||||
uploadedFile, err := rDocEdit.CoverFile.Open()
|
||||
if err != nil {
|
||||
log.Error("[createAppResourcesRoute] File Error")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
fileMime, err := mimetype.DetectReader(uploadedFile)
|
||||
if err != nil {
|
||||
log.Error("[createAppResourcesRoute] MIME Error")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
fileExtension := fileMime.Extension()
|
||||
|
||||
// Validate Extension
|
||||
if !slices.Contains([]string{".jpg", ".png"}, fileExtension) {
|
||||
log.Error("[uploadDocumentFile] Invalid FileType: ", fileExtension)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Filetype"})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate Storage Path
|
||||
fileName := fmt.Sprintf("%s%s", rDocID.DocumentID, fileExtension)
|
||||
safePath := filepath.Join(api.Config.DataPath, "covers", fileName)
|
||||
|
||||
// Save
|
||||
err = c.SaveUploadedFile(rDocEdit.CoverFile, safePath)
|
||||
if err != nil {
|
||||
log.Error("[createAppResourcesRoute] File Error")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
coverFileName = &fileName
|
||||
}
|
||||
|
||||
// Update Document
|
||||
if _, err := api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
|
||||
ID: rDocID.DocumentID,
|
||||
Title: api.sanitizeInput(rDocEdit.Title),
|
||||
Author: api.sanitizeInput(rDocEdit.Author),
|
||||
Description: api.sanitizeInput(rDocEdit.Description),
|
||||
Coverfile: coverFileName,
|
||||
}); err != nil {
|
||||
log.Error("[createAppResourcesRoute] UpsertDocument DB Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "./")
|
||||
return
|
||||
} else if routeName == "activity" {
|
||||
activityFilter := database.GetActivityParams{
|
||||
UserID: rUser.(string),
|
||||
@ -164,19 +262,19 @@ func (api *API) getDocumentCover(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Handle Identified Document
|
||||
if document.Olid != nil {
|
||||
if *document.Olid == "UNKNOWN" {
|
||||
if document.Coverfile != nil {
|
||||
if *document.Coverfile == "UNKNOWN" {
|
||||
c.File("./assets/no-cover.jpg")
|
||||
return
|
||||
}
|
||||
|
||||
// Derive Path
|
||||
fileName := "." + filepath.Clean(fmt.Sprintf("/%s.jpg", *document.Olid))
|
||||
safePath := filepath.Join(api.Config.DataPath, "covers", fileName)
|
||||
safePath := filepath.Join(api.Config.DataPath, "covers", *document.Coverfile)
|
||||
|
||||
// Validate File Exists
|
||||
_, err = os.Stat(safePath)
|
||||
if err != nil {
|
||||
log.Error("[getDocumentCover] File Should But Doesn't Exist:", err)
|
||||
c.File("./assets/no-cover.jpg")
|
||||
return
|
||||
}
|
||||
@ -185,59 +283,126 @@ func (api *API) getDocumentCover(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
This is a bit convoluted because we want to ensure we set the OLID to
|
||||
UNKNOWN if there are any errors. This will ideally prevent us from
|
||||
hitting the OpenLibrary API multiple times in the future.
|
||||
*/
|
||||
// --- Attempt Metadata ---
|
||||
|
||||
var coverID string = "UNKNOWN"
|
||||
var coverFilePath string
|
||||
var coverDir string = filepath.Join(api.Config.DataPath, "covers")
|
||||
var coverFile string = "UNKNOWN"
|
||||
|
||||
// Identify Documents & Save Covers
|
||||
bookMetadata := metadata.MetadataInfo{
|
||||
metadataResults, err := metadata.GetMetadata(metadata.MetadataInfo{
|
||||
Title: document.Title,
|
||||
Author: document.Author,
|
||||
}
|
||||
err = metadata.GetMetadata(&bookMetadata)
|
||||
if err == nil && bookMetadata.GBID != nil {
|
||||
// Derive & Sanitize File Name
|
||||
fileName := "." + filepath.Clean(fmt.Sprintf("/%s.jpg", *bookMetadata.GBID))
|
||||
})
|
||||
|
||||
// Generate Storage Path
|
||||
coverFilePath = filepath.Join(api.Config.DataPath, "covers", fileName)
|
||||
if err == nil && len(metadataResults) > 0 && metadataResults[0].GBID != nil {
|
||||
firstResult := metadataResults[0]
|
||||
|
||||
err := metadata.SaveCover(*bookMetadata.GBID, coverFilePath)
|
||||
// Save Cover
|
||||
fileName, err := metadata.SaveCover(*firstResult.GBID, coverDir, document.ID)
|
||||
if err == nil {
|
||||
coverID = *bookMetadata.GBID
|
||||
log.Info("Title:", *bookMetadata.Title)
|
||||
log.Info("Author:", *bookMetadata.Author)
|
||||
log.Info("Description:", *bookMetadata.Description)
|
||||
log.Info("IDs:", bookMetadata.ISBN)
|
||||
}
|
||||
coverFile = *fileName
|
||||
}
|
||||
|
||||
// coverIDs, err := metadata.GetCoverOLIDs(document.Title, document.Author)
|
||||
// if err == nil && len(coverIDs) > 0 {
|
||||
// coverFilePath, err = metadata.DownloadAndSaveCover(coverIDs[0], api.Config.DataPath)
|
||||
// if err == nil {
|
||||
// coverID = coverIDs[0]
|
||||
// }
|
||||
// }
|
||||
// Store First Metadata Result
|
||||
if _, err = api.DB.Queries.AddMetadata(api.DB.Ctx, database.AddMetadataParams{
|
||||
DocumentID: document.ID,
|
||||
Title: firstResult.Title,
|
||||
Author: firstResult.Author,
|
||||
Description: firstResult.Description,
|
||||
Gbid: firstResult.GBID,
|
||||
Olid: firstResult.OLID,
|
||||
Isbn10: firstResult.ISBN10,
|
||||
Isbn13: firstResult.ISBN13,
|
||||
}); err != nil {
|
||||
log.Error("[getDocumentCover] AddMetadata DB Error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Upsert Document
|
||||
if _, err = api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
|
||||
ID: document.ID,
|
||||
Olid: &coverID,
|
||||
Coverfile: &coverFile,
|
||||
}); err != nil {
|
||||
log.Warn("[getDocumentCover] UpsertDocument DB Error:", err)
|
||||
}
|
||||
|
||||
// Return Unknown Cover
|
||||
if coverID == "UNKNOWN" {
|
||||
if coverFile == "UNKNOWN" {
|
||||
c.File("./assets/no-cover.jpg")
|
||||
return
|
||||
}
|
||||
|
||||
coverFilePath := filepath.Join(coverDir, coverFile)
|
||||
c.File(coverFilePath)
|
||||
}
|
||||
|
||||
// DELETE /api/documents/:document
|
||||
func (api *API) deleteDocument(c *gin.Context) {
|
||||
var rDocID requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDocID); err != nil {
|
||||
log.Error("[deleteDocument] Invalid URI Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
changed, err := api.DB.Queries.DeleteDocument(api.DB.Ctx, rDocID.DocumentID)
|
||||
if err != nil {
|
||||
log.Error("[deleteDocument] DeleteDocument DB Error")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
if changed == 0 {
|
||||
log.Error("[deleteDocument] DeleteDocument DB Error")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Document"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "../")
|
||||
}
|
||||
|
||||
// POST /api/documents/:document/identify
|
||||
func (api *API) identifyDocument(c *gin.Context) {
|
||||
var rDocID requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDocID); err != nil {
|
||||
log.Error("[identifyDocument] Invalid URI Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
isbn := strings.TrimSpace(c.PostForm("ISBN"))
|
||||
if isbn == "" {
|
||||
log.Error("[identifyDocument] Invalid Form")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
metadataResults, err := metadata.GetMetadata(metadata.MetadataInfo{
|
||||
ISBN10: &isbn,
|
||||
ISBN13: &isbn,
|
||||
})
|
||||
if err != nil || len(metadataResults) == 0 {
|
||||
log.Error("[identifyDocument] Metadata Error")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Metadata Error"})
|
||||
return
|
||||
}
|
||||
|
||||
// TODO
|
||||
firstResult := metadataResults[0]
|
||||
|
||||
if firstResult.Title != nil {
|
||||
log.Info("Title:", *firstResult.Title)
|
||||
}
|
||||
if firstResult.Author != nil {
|
||||
log.Info("Author:", *firstResult.Author)
|
||||
}
|
||||
if firstResult.Description != nil {
|
||||
log.Info("Description:", *firstResult.Description)
|
||||
}
|
||||
if firstResult.ISBN10 != nil {
|
||||
log.Info("ISBN 10:", *firstResult.ISBN10)
|
||||
}
|
||||
if firstResult.ISBN13 != nil {
|
||||
log.Info("ISBN 13:", *firstResult.ISBN13)
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -72,8 +73,6 @@ type requestDocumentID struct {
|
||||
DocumentID string `uri:"document" binding:"required"`
|
||||
}
|
||||
|
||||
var allowedExtensions []string = []string{".epub", ".html"}
|
||||
|
||||
func (api *API) authorizeUser(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"authorized": "OK",
|
||||
@ -485,7 +484,7 @@ func (api *API) uploadDocumentFile(c *gin.Context) {
|
||||
fileMime, err := mimetype.DetectReader(uploadedFile)
|
||||
fileExtension := fileMime.Extension()
|
||||
|
||||
if !slices.Contains(allowedExtensions, fileExtension) {
|
||||
if !slices.Contains([]string{".epub", ".html"}, fileExtension) {
|
||||
log.Error("[uploadDocumentFile] Invalid FileType:", fileExtension)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Filetype"})
|
||||
return
|
||||
@ -609,12 +608,12 @@ func (api *API) sanitizeInput(val any) *string {
|
||||
switch v := val.(type) {
|
||||
case *string:
|
||||
if v != nil {
|
||||
newString := api.HTMLPolicy.Sanitize(string(*v))
|
||||
newString := html.UnescapeString(api.HTMLPolicy.Sanitize(string(*v)))
|
||||
return &newString
|
||||
}
|
||||
case string:
|
||||
if v != "" {
|
||||
newString := api.HTMLPolicy.Sanitize(string(v))
|
||||
newString := html.UnescapeString(api.HTMLPolicy.Sanitize(string(v)))
|
||||
return &newString
|
||||
}
|
||||
}
|
||||
|
@ -32,13 +32,17 @@ type Document struct {
|
||||
ID string `json:"id"`
|
||||
Md5 *string `json:"md5"`
|
||||
Filepath *string `json:"filepath"`
|
||||
Coverfile *string `json:"coverfile"`
|
||||
Title *string `json:"title"`
|
||||
Author *string `json:"author"`
|
||||
Series *string `json:"series"`
|
||||
SeriesIndex *int64 `json:"series_index"`
|
||||
Lang *string `json:"lang"`
|
||||
Description *string `json:"description"`
|
||||
Gbid *string `json:"gbid"`
|
||||
Olid *string `json:"-"`
|
||||
Isbn10 *string `json:"isbn10"`
|
||||
Isbn13 *string `json:"isbn13"`
|
||||
Synced bool `json:"-"`
|
||||
Deleted bool `json:"-"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
@ -62,6 +66,19 @@ type DocumentProgress struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type Metadatum struct {
|
||||
ID int64 `json:"id"`
|
||||
DocumentID string `json:"document_id"`
|
||||
Title *string `json:"title"`
|
||||
Author *string `json:"author"`
|
||||
Description *string `json:"description"`
|
||||
Gbid *string `json:"gbid"`
|
||||
Olid *string `json:"olid"`
|
||||
Isbn10 *string `json:"isbn10"`
|
||||
Isbn13 *string `json:"isbn13"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type RescaledActivity struct {
|
||||
DocumentID string `json:"document_id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
|
@ -1,3 +1,17 @@
|
||||
-- name: AddMetadata :one
|
||||
INSERT INTO metadata (
|
||||
document_id,
|
||||
title,
|
||||
author,
|
||||
description,
|
||||
gbid,
|
||||
olid,
|
||||
isbn10,
|
||||
isbn13
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING *;
|
||||
|
||||
-- name: CreateUser :execrows
|
||||
INSERT INTO users (id, pass)
|
||||
VALUES (?, ?)
|
||||
@ -12,26 +26,34 @@ INSERT INTO documents (
|
||||
id,
|
||||
md5,
|
||||
filepath,
|
||||
coverfile,
|
||||
title,
|
||||
author,
|
||||
series,
|
||||
series_index,
|
||||
lang,
|
||||
description,
|
||||
olid
|
||||
olid,
|
||||
gbid,
|
||||
isbn10,
|
||||
isbn13
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT DO UPDATE
|
||||
SET
|
||||
md5 = COALESCE(excluded.md5, md5),
|
||||
filepath = COALESCE(excluded.filepath, filepath),
|
||||
coverfile = COALESCE(excluded.coverfile, coverfile),
|
||||
title = COALESCE(excluded.title, title),
|
||||
author = COALESCE(excluded.author, author),
|
||||
series = COALESCE(excluded.series, series),
|
||||
series_index = COALESCE(excluded.series_index, series_index),
|
||||
lang = COALESCE(excluded.lang, lang),
|
||||
description = COALESCE(excluded.description, description),
|
||||
olid = COALESCE(excluded.olid, olid)
|
||||
olid = COALESCE(excluded.olid, olid),
|
||||
gbid = COALESCE(excluded.gbid, gbid),
|
||||
isbn10 = COALESCE(excluded.isbn10, isbn10),
|
||||
isbn13 = COALESCE(excluded.isbn13, isbn13)
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteDocument :execrows
|
||||
@ -222,6 +244,7 @@ SELECT
|
||||
FROM documents
|
||||
LEFT JOIN true_progress ON true_progress.document_id = documents.id
|
||||
LEFT JOIN users ON users.id = $user_id
|
||||
WHERE documents.deleted == false
|
||||
ORDER BY true_progress.last_read DESC, documents.created_at DESC
|
||||
LIMIT $limit
|
||||
OFFSET $offset;
|
||||
|
@ -61,6 +61,59 @@ func (q *Queries) AddActivity(ctx context.Context, arg AddActivityParams) (Activ
|
||||
return i, err
|
||||
}
|
||||
|
||||
const addMetadata = `-- name: AddMetadata :one
|
||||
INSERT INTO metadata (
|
||||
document_id,
|
||||
title,
|
||||
author,
|
||||
description,
|
||||
gbid,
|
||||
olid,
|
||||
isbn10,
|
||||
isbn13
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING id, document_id, title, author, description, gbid, olid, isbn10, isbn13, created_at
|
||||
`
|
||||
|
||||
type AddMetadataParams struct {
|
||||
DocumentID string `json:"document_id"`
|
||||
Title *string `json:"title"`
|
||||
Author *string `json:"author"`
|
||||
Description *string `json:"description"`
|
||||
Gbid *string `json:"gbid"`
|
||||
Olid *string `json:"olid"`
|
||||
Isbn10 *string `json:"isbn10"`
|
||||
Isbn13 *string `json:"isbn13"`
|
||||
}
|
||||
|
||||
func (q *Queries) AddMetadata(ctx context.Context, arg AddMetadataParams) (Metadatum, error) {
|
||||
row := q.db.QueryRowContext(ctx, addMetadata,
|
||||
arg.DocumentID,
|
||||
arg.Title,
|
||||
arg.Author,
|
||||
arg.Description,
|
||||
arg.Gbid,
|
||||
arg.Olid,
|
||||
arg.Isbn10,
|
||||
arg.Isbn13,
|
||||
)
|
||||
var i Metadatum
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.DocumentID,
|
||||
&i.Title,
|
||||
&i.Author,
|
||||
&i.Description,
|
||||
&i.Gbid,
|
||||
&i.Olid,
|
||||
&i.Isbn10,
|
||||
&i.Isbn13,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const createUser = `-- name: CreateUser :execrows
|
||||
INSERT INTO users (id, pass)
|
||||
VALUES (?, ?)
|
||||
@ -366,7 +419,7 @@ func (q *Queries) GetDevices(ctx context.Context, arg GetDevicesParams) ([]Devic
|
||||
}
|
||||
|
||||
const getDocument = `-- name: GetDocument :one
|
||||
SELECT id, md5, filepath, title, author, series, series_index, lang, description, olid, synced, deleted, updated_at, created_at FROM documents
|
||||
SELECT id, md5, filepath, coverfile, title, author, series, series_index, lang, description, gbid, olid, isbn10, isbn13, synced, deleted, updated_at, created_at FROM documents
|
||||
WHERE id = ?1 LIMIT 1
|
||||
`
|
||||
|
||||
@ -377,13 +430,17 @@ func (q *Queries) GetDocument(ctx context.Context, documentID string) (Document,
|
||||
&i.ID,
|
||||
&i.Md5,
|
||||
&i.Filepath,
|
||||
&i.Coverfile,
|
||||
&i.Title,
|
||||
&i.Author,
|
||||
&i.Series,
|
||||
&i.SeriesIndex,
|
||||
&i.Lang,
|
||||
&i.Description,
|
||||
&i.Gbid,
|
||||
&i.Olid,
|
||||
&i.Isbn10,
|
||||
&i.Isbn13,
|
||||
&i.Synced,
|
||||
&i.Deleted,
|
||||
&i.UpdatedAt,
|
||||
@ -501,7 +558,7 @@ WITH true_progress AS (
|
||||
LIMIT 1
|
||||
)
|
||||
SELECT
|
||||
documents.id, documents.md5, documents.filepath, documents.title, documents.author, documents.series, documents.series_index, documents.lang, documents.description, documents.olid, documents.synced, documents.deleted, documents.updated_at, documents.created_at,
|
||||
documents.id, documents.md5, documents.filepath, documents.coverfile, documents.title, documents.author, documents.series, documents.series_index, documents.lang, documents.description, documents.gbid, documents.olid, documents.isbn10, documents.isbn13, documents.synced, documents.deleted, documents.updated_at, documents.created_at,
|
||||
|
||||
CAST(IFNULL(current_page, 0) AS INTEGER) AS current_page,
|
||||
CAST(IFNULL(total_pages, 0) AS INTEGER) AS total_pages,
|
||||
@ -531,13 +588,17 @@ type GetDocumentWithStatsRow struct {
|
||||
ID string `json:"id"`
|
||||
Md5 *string `json:"md5"`
|
||||
Filepath *string `json:"filepath"`
|
||||
Coverfile *string `json:"coverfile"`
|
||||
Title *string `json:"title"`
|
||||
Author *string `json:"author"`
|
||||
Series *string `json:"series"`
|
||||
SeriesIndex *int64 `json:"series_index"`
|
||||
Lang *string `json:"lang"`
|
||||
Description *string `json:"description"`
|
||||
Gbid *string `json:"gbid"`
|
||||
Olid *string `json:"-"`
|
||||
Isbn10 *string `json:"isbn10"`
|
||||
Isbn13 *string `json:"isbn13"`
|
||||
Synced bool `json:"-"`
|
||||
Deleted bool `json:"-"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
@ -556,13 +617,17 @@ func (q *Queries) GetDocumentWithStats(ctx context.Context, arg GetDocumentWithS
|
||||
&i.ID,
|
||||
&i.Md5,
|
||||
&i.Filepath,
|
||||
&i.Coverfile,
|
||||
&i.Title,
|
||||
&i.Author,
|
||||
&i.Series,
|
||||
&i.SeriesIndex,
|
||||
&i.Lang,
|
||||
&i.Description,
|
||||
&i.Gbid,
|
||||
&i.Olid,
|
||||
&i.Isbn10,
|
||||
&i.Isbn13,
|
||||
&i.Synced,
|
||||
&i.Deleted,
|
||||
&i.UpdatedAt,
|
||||
@ -577,7 +642,7 @@ func (q *Queries) GetDocumentWithStats(ctx context.Context, arg GetDocumentWithS
|
||||
}
|
||||
|
||||
const getDocuments = `-- name: GetDocuments :many
|
||||
SELECT id, md5, filepath, title, author, series, series_index, lang, description, olid, synced, deleted, updated_at, created_at FROM documents
|
||||
SELECT id, md5, filepath, coverfile, title, author, series, series_index, lang, description, gbid, olid, isbn10, isbn13, synced, deleted, updated_at, created_at FROM documents
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?2
|
||||
OFFSET ?1
|
||||
@ -601,13 +666,17 @@ func (q *Queries) GetDocuments(ctx context.Context, arg GetDocumentsParams) ([]D
|
||||
&i.ID,
|
||||
&i.Md5,
|
||||
&i.Filepath,
|
||||
&i.Coverfile,
|
||||
&i.Title,
|
||||
&i.Author,
|
||||
&i.Series,
|
||||
&i.SeriesIndex,
|
||||
&i.Lang,
|
||||
&i.Description,
|
||||
&i.Gbid,
|
||||
&i.Olid,
|
||||
&i.Isbn10,
|
||||
&i.Isbn13,
|
||||
&i.Synced,
|
||||
&i.Deleted,
|
||||
&i.UpdatedAt,
|
||||
@ -641,7 +710,7 @@ WITH true_progress AS (
|
||||
HAVING MAX(start_time)
|
||||
)
|
||||
SELECT
|
||||
documents.id, documents.md5, documents.filepath, documents.title, documents.author, documents.series, documents.series_index, documents.lang, documents.description, documents.olid, documents.synced, documents.deleted, documents.updated_at, documents.created_at,
|
||||
documents.id, documents.md5, documents.filepath, documents.coverfile, documents.title, documents.author, documents.series, documents.series_index, documents.lang, documents.description, documents.gbid, documents.olid, documents.isbn10, documents.isbn13, documents.synced, documents.deleted, documents.updated_at, documents.created_at,
|
||||
|
||||
CAST(IFNULL(current_page, 0) AS INTEGER) AS current_page,
|
||||
CAST(IFNULL(total_pages, 0) AS INTEGER) AS total_pages,
|
||||
@ -657,6 +726,7 @@ SELECT
|
||||
FROM documents
|
||||
LEFT JOIN true_progress ON true_progress.document_id = documents.id
|
||||
LEFT JOIN users ON users.id = ?1
|
||||
WHERE documents.deleted == false
|
||||
ORDER BY true_progress.last_read DESC, documents.created_at DESC
|
||||
LIMIT ?3
|
||||
OFFSET ?2
|
||||
@ -672,13 +742,17 @@ type GetDocumentsWithStatsRow struct {
|
||||
ID string `json:"id"`
|
||||
Md5 *string `json:"md5"`
|
||||
Filepath *string `json:"filepath"`
|
||||
Coverfile *string `json:"coverfile"`
|
||||
Title *string `json:"title"`
|
||||
Author *string `json:"author"`
|
||||
Series *string `json:"series"`
|
||||
SeriesIndex *int64 `json:"series_index"`
|
||||
Lang *string `json:"lang"`
|
||||
Description *string `json:"description"`
|
||||
Gbid *string `json:"gbid"`
|
||||
Olid *string `json:"-"`
|
||||
Isbn10 *string `json:"isbn10"`
|
||||
Isbn13 *string `json:"isbn13"`
|
||||
Synced bool `json:"-"`
|
||||
Deleted bool `json:"-"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
@ -703,13 +777,17 @@ func (q *Queries) GetDocumentsWithStats(ctx context.Context, arg GetDocumentsWit
|
||||
&i.ID,
|
||||
&i.Md5,
|
||||
&i.Filepath,
|
||||
&i.Coverfile,
|
||||
&i.Title,
|
||||
&i.Author,
|
||||
&i.Series,
|
||||
&i.SeriesIndex,
|
||||
&i.Lang,
|
||||
&i.Description,
|
||||
&i.Gbid,
|
||||
&i.Olid,
|
||||
&i.Isbn10,
|
||||
&i.Isbn13,
|
||||
&i.Synced,
|
||||
&i.Deleted,
|
||||
&i.UpdatedAt,
|
||||
@ -754,7 +832,7 @@ func (q *Queries) GetLastActivity(ctx context.Context, arg GetLastActivityParams
|
||||
}
|
||||
|
||||
const getMissingDocuments = `-- name: GetMissingDocuments :many
|
||||
SELECT documents.id, documents.md5, documents.filepath, documents.title, documents.author, documents.series, documents.series_index, documents.lang, documents.description, documents.olid, documents.synced, documents.deleted, documents.updated_at, documents.created_at FROM documents
|
||||
SELECT documents.id, documents.md5, documents.filepath, documents.coverfile, documents.title, documents.author, documents.series, documents.series_index, documents.lang, documents.description, documents.gbid, documents.olid, documents.isbn10, documents.isbn13, documents.synced, documents.deleted, documents.updated_at, documents.created_at FROM documents
|
||||
WHERE
|
||||
documents.filepath IS NOT NULL
|
||||
AND documents.deleted = false
|
||||
@ -784,13 +862,17 @@ func (q *Queries) GetMissingDocuments(ctx context.Context, documentIds []string)
|
||||
&i.ID,
|
||||
&i.Md5,
|
||||
&i.Filepath,
|
||||
&i.Coverfile,
|
||||
&i.Title,
|
||||
&i.Author,
|
||||
&i.Series,
|
||||
&i.SeriesIndex,
|
||||
&i.Lang,
|
||||
&i.Description,
|
||||
&i.Gbid,
|
||||
&i.Olid,
|
||||
&i.Isbn10,
|
||||
&i.Isbn13,
|
||||
&i.Synced,
|
||||
&i.Deleted,
|
||||
&i.UpdatedAt,
|
||||
@ -1073,7 +1155,7 @@ UPDATE documents
|
||||
SET
|
||||
deleted = ?1
|
||||
WHERE id = ?2
|
||||
RETURNING id, md5, filepath, title, author, series, series_index, lang, description, olid, synced, deleted, updated_at, created_at
|
||||
RETURNING id, md5, filepath, coverfile, title, author, series, series_index, lang, description, gbid, olid, isbn10, isbn13, synced, deleted, updated_at, created_at
|
||||
`
|
||||
|
||||
type UpdateDocumentDeletedParams struct {
|
||||
@ -1088,13 +1170,17 @@ func (q *Queries) UpdateDocumentDeleted(ctx context.Context, arg UpdateDocumentD
|
||||
&i.ID,
|
||||
&i.Md5,
|
||||
&i.Filepath,
|
||||
&i.Coverfile,
|
||||
&i.Title,
|
||||
&i.Author,
|
||||
&i.Series,
|
||||
&i.SeriesIndex,
|
||||
&i.Lang,
|
||||
&i.Description,
|
||||
&i.Gbid,
|
||||
&i.Olid,
|
||||
&i.Isbn10,
|
||||
&i.Isbn13,
|
||||
&i.Synced,
|
||||
&i.Deleted,
|
||||
&i.UpdatedAt,
|
||||
@ -1108,7 +1194,7 @@ UPDATE documents
|
||||
SET
|
||||
synced = ?1
|
||||
WHERE id = ?2
|
||||
RETURNING id, md5, filepath, title, author, series, series_index, lang, description, olid, synced, deleted, updated_at, created_at
|
||||
RETURNING id, md5, filepath, coverfile, title, author, series, series_index, lang, description, gbid, olid, isbn10, isbn13, synced, deleted, updated_at, created_at
|
||||
`
|
||||
|
||||
type UpdateDocumentSyncParams struct {
|
||||
@ -1123,13 +1209,17 @@ func (q *Queries) UpdateDocumentSync(ctx context.Context, arg UpdateDocumentSync
|
||||
&i.ID,
|
||||
&i.Md5,
|
||||
&i.Filepath,
|
||||
&i.Coverfile,
|
||||
&i.Title,
|
||||
&i.Author,
|
||||
&i.Series,
|
||||
&i.SeriesIndex,
|
||||
&i.Lang,
|
||||
&i.Description,
|
||||
&i.Gbid,
|
||||
&i.Olid,
|
||||
&i.Isbn10,
|
||||
&i.Isbn13,
|
||||
&i.Synced,
|
||||
&i.Deleted,
|
||||
&i.UpdatedAt,
|
||||
@ -1211,33 +1301,42 @@ INSERT INTO documents (
|
||||
id,
|
||||
md5,
|
||||
filepath,
|
||||
coverfile,
|
||||
title,
|
||||
author,
|
||||
series,
|
||||
series_index,
|
||||
lang,
|
||||
description,
|
||||
olid
|
||||
olid,
|
||||
gbid,
|
||||
isbn10,
|
||||
isbn13
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT DO UPDATE
|
||||
SET
|
||||
md5 = COALESCE(excluded.md5, md5),
|
||||
filepath = COALESCE(excluded.filepath, filepath),
|
||||
coverfile = COALESCE(excluded.coverfile, coverfile),
|
||||
title = COALESCE(excluded.title, title),
|
||||
author = COALESCE(excluded.author, author),
|
||||
series = COALESCE(excluded.series, series),
|
||||
series_index = COALESCE(excluded.series_index, series_index),
|
||||
lang = COALESCE(excluded.lang, lang),
|
||||
description = COALESCE(excluded.description, description),
|
||||
olid = COALESCE(excluded.olid, olid)
|
||||
RETURNING id, md5, filepath, title, author, series, series_index, lang, description, olid, synced, deleted, updated_at, created_at
|
||||
olid = COALESCE(excluded.olid, olid),
|
||||
gbid = COALESCE(excluded.gbid, gbid),
|
||||
isbn10 = COALESCE(excluded.isbn10, isbn10),
|
||||
isbn13 = COALESCE(excluded.isbn13, isbn13)
|
||||
RETURNING id, md5, filepath, coverfile, title, author, series, series_index, lang, description, gbid, olid, isbn10, isbn13, synced, deleted, updated_at, created_at
|
||||
`
|
||||
|
||||
type UpsertDocumentParams struct {
|
||||
ID string `json:"id"`
|
||||
Md5 *string `json:"md5"`
|
||||
Filepath *string `json:"filepath"`
|
||||
Coverfile *string `json:"coverfile"`
|
||||
Title *string `json:"title"`
|
||||
Author *string `json:"author"`
|
||||
Series *string `json:"series"`
|
||||
@ -1245,6 +1344,9 @@ type UpsertDocumentParams struct {
|
||||
Lang *string `json:"lang"`
|
||||
Description *string `json:"description"`
|
||||
Olid *string `json:"-"`
|
||||
Gbid *string `json:"gbid"`
|
||||
Isbn10 *string `json:"isbn10"`
|
||||
Isbn13 *string `json:"isbn13"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertDocument(ctx context.Context, arg UpsertDocumentParams) (Document, error) {
|
||||
@ -1252,6 +1354,7 @@ func (q *Queries) UpsertDocument(ctx context.Context, arg UpsertDocumentParams)
|
||||
arg.ID,
|
||||
arg.Md5,
|
||||
arg.Filepath,
|
||||
arg.Coverfile,
|
||||
arg.Title,
|
||||
arg.Author,
|
||||
arg.Series,
|
||||
@ -1259,19 +1362,26 @@ func (q *Queries) UpsertDocument(ctx context.Context, arg UpsertDocumentParams)
|
||||
arg.Lang,
|
||||
arg.Description,
|
||||
arg.Olid,
|
||||
arg.Gbid,
|
||||
arg.Isbn10,
|
||||
arg.Isbn13,
|
||||
)
|
||||
var i Document
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Md5,
|
||||
&i.Filepath,
|
||||
&i.Coverfile,
|
||||
&i.Title,
|
||||
&i.Author,
|
||||
&i.Series,
|
||||
&i.SeriesIndex,
|
||||
&i.Lang,
|
||||
&i.Description,
|
||||
&i.Gbid,
|
||||
&i.Olid,
|
||||
&i.Isbn10,
|
||||
&i.Isbn13,
|
||||
&i.Synced,
|
||||
&i.Deleted,
|
||||
&i.UpdatedAt,
|
||||
|
@ -18,13 +18,19 @@ CREATE TABLE IF NOT EXISTS documents (
|
||||
|
||||
md5 TEXT,
|
||||
filepath TEXT,
|
||||
coverfile TEXT,
|
||||
title TEXT,
|
||||
author TEXT,
|
||||
series TEXT,
|
||||
series_index INTEGER,
|
||||
lang TEXT,
|
||||
description TEXT,
|
||||
|
||||
gbid TEXT,
|
||||
olid TEXT,
|
||||
isbn10 TEXT,
|
||||
isbn13 TEXT,
|
||||
|
||||
synced BOOLEAN NOT NULL DEFAULT 0 CHECK (synced IN (0, 1)),
|
||||
deleted BOOLEAN NOT NULL DEFAULT 0 CHECK (deleted IN (0, 1)),
|
||||
|
||||
@ -32,6 +38,25 @@ CREATE TABLE IF NOT EXISTS documents (
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Metadata
|
||||
CREATE TABLE IF NOT EXISTS metadata (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
document_id TEXT NOT NULL,
|
||||
|
||||
title TEXT,
|
||||
author TEXT,
|
||||
description TEXT,
|
||||
gbid TEXT,
|
||||
olid TEXT,
|
||||
isbn10 TEXT,
|
||||
isbn13 TEXT,
|
||||
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (document_id) REFERENCES documents (id)
|
||||
);
|
||||
|
||||
-- Devices
|
||||
CREATE TABLE IF NOT EXISTS devices (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -17,7 +18,9 @@ type MetadataInfo struct {
|
||||
Author *string
|
||||
Description *string
|
||||
GBID *string
|
||||
ISBN []*string
|
||||
OLID *string
|
||||
ISBN10 *string
|
||||
ISBN13 *string
|
||||
}
|
||||
|
||||
type gBooksIdentifiers struct {
|
||||
@ -46,73 +49,99 @@ const GBOOKS_QUERY_URL string = "https://www.googleapis.com/books/v1/volumes?q=%
|
||||
const GBOOKS_GBID_INFO_URL string = "https://www.googleapis.com/books/v1/volumes/%s"
|
||||
const GBOOKS_GBID_COVER_URL string = "https://books.google.com/books/content/images/frontcover/%s?fife=w480-h690"
|
||||
|
||||
func GetMetadata(data *MetadataInfo) error {
|
||||
var queryResult *gBooksQueryItem
|
||||
if data.GBID != nil {
|
||||
func GetMetadata(metadataSearch MetadataInfo) ([]MetadataInfo, error) {
|
||||
var queryResults []gBooksQueryItem
|
||||
if metadataSearch.GBID != nil {
|
||||
// Use GBID
|
||||
resp, err := performGBIDRequest(*data.GBID)
|
||||
resp, err := performGBIDRequest(*metadataSearch.GBID)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
queryResult = resp
|
||||
} else if len(data.ISBN) > 0 {
|
||||
searchQuery := "isbn:" + *data.ISBN[0]
|
||||
queryResults = []gBooksQueryItem{*resp}
|
||||
} else if metadataSearch.ISBN13 != nil {
|
||||
searchQuery := "isbn:" + *metadataSearch.ISBN13
|
||||
resp, err := performSearchRequest(searchQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
queryResult = &resp.Items[0]
|
||||
} else if data.Title != nil && data.Author != nil {
|
||||
searchQuery := url.QueryEscape(fmt.Sprintf("%s %s", *data.Title, *data.Author))
|
||||
queryResults = resp.Items
|
||||
} else if metadataSearch.ISBN10 != nil {
|
||||
searchQuery := "isbn:" + *metadataSearch.ISBN10
|
||||
resp, err := performSearchRequest(searchQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
queryResult = &resp.Items[0]
|
||||
queryResults = resp.Items
|
||||
} else if metadataSearch.Title != nil && metadataSearch.Author != nil {
|
||||
searchQuery := url.QueryEscape(fmt.Sprintf("%s %s", *metadataSearch.Title, *metadataSearch.Author))
|
||||
resp, err := performSearchRequest(searchQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
queryResults = resp.Items
|
||||
} else {
|
||||
return errors.New("Invalid Data")
|
||||
return nil, errors.New("Invalid Data")
|
||||
}
|
||||
|
||||
// Merge Data
|
||||
data.GBID = &queryResult.ID
|
||||
data.Description = &queryResult.Info.Description
|
||||
data.Title = &queryResult.Info.Title
|
||||
if len(queryResult.Info.Authors) > 0 {
|
||||
data.Author = &queryResult.Info.Authors[0]
|
||||
}
|
||||
for _, item := range queryResult.Info.Identifiers {
|
||||
if item.Type == "ISBN_10" || item.Type == "ISBN_13" {
|
||||
data.ISBN = append(data.ISBN, &item.Identifier)
|
||||
// Normalize Data
|
||||
allMetadata := []MetadataInfo{}
|
||||
|
||||
for i := range queryResults {
|
||||
// Account for range value changing every iteration
|
||||
item := queryResults[i]
|
||||
itemResult := MetadataInfo{
|
||||
GBID: &item.ID,
|
||||
Title: &item.Info.Title,
|
||||
Description: &item.Info.Description,
|
||||
}
|
||||
|
||||
if len(item.Info.Authors) > 0 {
|
||||
itemResult.Author = &item.Info.Authors[0]
|
||||
}
|
||||
for _, item := range item.Info.Identifiers {
|
||||
if itemResult.ISBN10 != nil && itemResult.ISBN13 != nil {
|
||||
break
|
||||
} else if itemResult.ISBN10 == nil && item.Type == "ISBN_10" {
|
||||
itemResult.ISBN10 = &item.Identifier
|
||||
} else if itemResult.ISBN13 == nil && item.Type == "ISBN_13" {
|
||||
itemResult.ISBN13 = &item.Identifier
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
allMetadata = append(allMetadata, itemResult)
|
||||
}
|
||||
|
||||
func SaveCover(id string, safePath string) error {
|
||||
return allMetadata, nil
|
||||
}
|
||||
|
||||
func SaveCover(gbid string, coverDir string, documentID string) (*string, error) {
|
||||
|
||||
// Google Books -> JPG
|
||||
coverFile := "." + filepath.Clean(fmt.Sprintf("/%s.jpg", documentID))
|
||||
coverFilePath := filepath.Join(coverDir, coverFile)
|
||||
|
||||
// Validate File Doesn't Exists
|
||||
_, err := os.Stat(safePath)
|
||||
_, err := os.Stat(coverFilePath)
|
||||
if err == nil {
|
||||
log.Warn("[SaveCover] File Alreads Exists")
|
||||
return nil
|
||||
return &coverFile, nil
|
||||
}
|
||||
|
||||
// Create File
|
||||
out, err := os.Create(safePath)
|
||||
out, err := os.Create(coverFilePath)
|
||||
if err != nil {
|
||||
log.Error("[SaveCover] File Create Error")
|
||||
return errors.New("File Failure")
|
||||
return nil, errors.New("File Failure")
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// Download File
|
||||
log.Info("[SaveCover] Downloading Cover")
|
||||
coverURL := fmt.Sprintf(GBOOKS_GBID_COVER_URL, id)
|
||||
coverURL := fmt.Sprintf(GBOOKS_GBID_COVER_URL, gbid)
|
||||
resp, err := http.Get(coverURL)
|
||||
if err != nil {
|
||||
log.Error("[SaveCover] Cover URL API Failure")
|
||||
return errors.New("API Failure")
|
||||
return nil, errors.New("API Failure")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
@ -121,20 +150,19 @@ func SaveCover(id string, safePath string) error {
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
log.Error("[SaveCover] File Copy Error")
|
||||
return errors.New("File Failure")
|
||||
return nil, errors.New("File Failure")
|
||||
}
|
||||
|
||||
// Return FilePath
|
||||
return nil
|
||||
return &coverFile, nil
|
||||
}
|
||||
|
||||
func performSearchRequest(searchQuery string) (*gBooksQueryResponse, error) {
|
||||
apiQuery := fmt.Sprintf(GBOOKS_QUERY_URL, searchQuery)
|
||||
|
||||
log.Info("[performSearchRequest] Acquiring CoverID")
|
||||
log.Info("[performSearchRequest] Acquiring Metadata: ", apiQuery)
|
||||
resp, err := http.Get(apiQuery)
|
||||
if err != nil {
|
||||
log.Error("[performSearchRequest] Cover URL API Failure")
|
||||
log.Error("[performSearchRequest] Google Books Query URL API Failure")
|
||||
return nil, errors.New("API Failure")
|
||||
}
|
||||
|
||||
|
45
sqlc.yaml
45
sqlc.yaml
@ -18,6 +18,10 @@ sql:
|
||||
go_type:
|
||||
type: "string"
|
||||
pointer: true
|
||||
- column: "documents.coverfile"
|
||||
go_type:
|
||||
type: "string"
|
||||
pointer: true
|
||||
- column: "documents.title"
|
||||
go_type:
|
||||
type: "string"
|
||||
@ -46,6 +50,47 @@ sql:
|
||||
go_type:
|
||||
type: "string"
|
||||
pointer: true
|
||||
- column: "documents.gbid"
|
||||
go_type:
|
||||
type: "string"
|
||||
pointer: true
|
||||
- column: "documents.isbn10"
|
||||
go_type:
|
||||
type: "string"
|
||||
pointer: true
|
||||
- column: "documents.isbn13"
|
||||
go_type:
|
||||
type: "string"
|
||||
pointer: true
|
||||
|
||||
- column: "metadata.title"
|
||||
go_type:
|
||||
type: "string"
|
||||
pointer: true
|
||||
- column: "metadata.author"
|
||||
go_type:
|
||||
type: "string"
|
||||
pointer: true
|
||||
- column: "metadata.description"
|
||||
go_type:
|
||||
type: "string"
|
||||
pointer: true
|
||||
- column: "metadata.gbid"
|
||||
go_type:
|
||||
type: "string"
|
||||
pointer: true
|
||||
- column: "metadata.olid"
|
||||
go_type:
|
||||
type: "string"
|
||||
pointer: true
|
||||
- column: "metadata.isbn10"
|
||||
go_type:
|
||||
type: "string"
|
||||
pointer: true
|
||||
- column: "metadata.isbn13"
|
||||
go_type:
|
||||
type: "string"
|
||||
pointer: true
|
||||
|
||||
# Do not generate JSON
|
||||
- column: "documents.synced"
|
||||
|
166
templates/document-edit.html
Normal file
166
templates/document-edit.html
Normal file
@ -0,0 +1,166 @@
|
||||
{{template "base.html" .}}
|
||||
|
||||
{{define "title"}}Documents{{end}}
|
||||
|
||||
{{define "header"}}
|
||||
<a href="../documents">Documents</a>
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="h-full w-full overflow-scroll bg-white shadow-lg dark:bg-gray-700 rounded dark:text-white p-6">
|
||||
<div class="flex flex-col gap-2 float-left mr-6 mb-6">
|
||||
<img class="rounded w-40 md:w-60 lg:w-80 object-fill h-full" src="../documents/{{.Data.ID}}/cover"></img>
|
||||
<div class="flex gap-2 justify-end text-gray-500 dark:text-gray-400">
|
||||
<div class="relative">
|
||||
<label for="delete-button">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
class="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3 6.52381C3 6.12932 3.32671 5.80952 3.72973 5.80952H8.51787C8.52437 4.9683 8.61554 3.81504 9.45037 3.01668C10.1074 2.38839 11.0081 2 12 2C12.9919 2 13.8926 2.38839 14.5496 3.01668C15.3844 3.81504 15.4756 4.9683 15.4821 5.80952H20.2703C20.6733 5.80952 21 6.12932 21 6.52381C21 6.9183 20.6733 7.2381 20.2703 7.2381H3.72973C3.32671 7.2381 3 6.9183 3 6.52381Z"
|
||||
/>
|
||||
<path
|
||||
d="M11.6066 22H12.3935C15.101 22 16.4547 22 17.3349 21.1368C18.2151 20.2736 18.3052 18.8576 18.4853 16.0257L18.7448 11.9452C18.8425 10.4086 18.8913 9.64037 18.4498 9.15352C18.0082 8.66667 17.2625 8.66667 15.7712 8.66667H8.22884C6.7375 8.66667 5.99183 8.66667 5.55026 9.15352C5.1087 9.64037 5.15756 10.4086 5.25528 11.9452L5.51479 16.0257C5.69489 18.8576 5.78494 20.2736 6.66513 21.1368C7.54532 22 8.89906 22 11.6066 22Z"
|
||||
/>
|
||||
</svg>
|
||||
</label>
|
||||
<input type="checkbox" id="delete-button" class="hidden css-button"/>
|
||||
<form
|
||||
method="POST"
|
||||
action="./{{ .Data.ID }}/delete"
|
||||
class="absolute bottom-7 left-5 text-black bg-gray-200 transition-all duration-200 rounded shadow-inner shadow-lg shadow-gray-500 dark:text-white dark:shadow-gray-900 dark:bg-gray-600 text-sm p-3"
|
||||
>
|
||||
<p class="font-medium w-24 pb-2">Are you sure?</p>
|
||||
<button class="font-medium w-full px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
<a href="../activity?document={{ .Data.ID }}">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
class="hover:text-gray-800 dark:hover:text-gray-100"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M9.5 2C8.67157 2 8 2.67157 8 3.5V4.5C8 5.32843 8.67157 6 9.5 6H14.5C15.3284 6 16 5.32843 16 4.5V3.5C16 2.67157 15.3284 2 14.5 2H9.5Z"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.5 4.03662C5.24209 4.10719 4.44798 4.30764 3.87868 4.87694C3 5.75562 3 7.16983 3 9.99826V15.9983C3 18.8267 3 20.2409 3.87868 21.1196C4.75736 21.9983 6.17157 21.9983 9 21.9983H15C17.8284 21.9983 19.2426 21.9983 20.1213 21.1196C21 20.2409 21 18.8267 21 15.9983V9.99826C21 7.16983 21 5.75562 20.1213 4.87694C19.552 4.30764 18.7579 4.10719 17.5 4.03662V4.5C17.5 6.15685 16.1569 7.5 14.5 7.5H9.5C7.84315 7.5 6.5 6.15685 6.5 4.5V4.03662ZM7 9.75C6.58579 9.75 6.25 10.0858 6.25 10.5C6.25 10.9142 6.58579 11.25 7 11.25H7.5C7.91421 11.25 8.25 10.9142 8.25 10.5C8.25 10.0858 7.91421 9.75 7.5 9.75H7ZM10.5 9.75C10.0858 9.75 9.75 10.0858 9.75 10.5C9.75 10.9142 10.0858 11.25 10.5 11.25H17C17.4142 11.25 17.75 10.9142 17.75 10.5C17.75 10.0858 17.4142 9.75 17 9.75H10.5ZM7 13.25C6.58579 13.25 6.25 13.5858 6.25 14C6.25 14.4142 6.58579 14.75 7 14.75H7.5C7.91421 14.75 8.25 14.4142 8.25 14C8.25 13.5858 7.91421 13.25 7.5 13.25H7ZM10.5 13.25C10.0858 13.25 9.75 13.5858 9.75 14C9.75 14.4142 10.0858 14.75 10.5 14.75H17C17.4142 14.75 17.75 14.4142 17.75 14C17.75 13.5858 17.4142 13.25 17 13.25H10.5ZM7 16.75C6.58579 16.75 6.25 17.0858 6.25 17.5C6.25 17.9142 6.58579 18.25 7 18.25H7.5C7.91421 18.25 8.25 17.9142 8.25 17.5C8.25 17.0858 7.91421 16.75 7.5 16.75H7ZM10.5 16.75C10.0858 16.75 9.75 17.0858 9.75 17.5C9.75 17.9142 10.0858 18.25 10.5 18.25H17C17.4142 18.25 17.75 17.9142 17.75 17.5C17.75 17.0858 17.4142 16.75 17 16.75H10.5Z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<div class="relative">
|
||||
<label for="edit-button">
|
||||
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
class="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M21.1938 2.80624C22.2687 3.88124 22.2687 5.62415 21.1938 6.69914L20.6982 7.19469C20.5539 7.16345 20.3722 7.11589 20.1651 7.04404C19.6108 6.85172 18.8823 6.48827 18.197 5.803C17.5117 5.11774 17.1483 4.38923 16.956 3.8349C16.8841 3.62781 16.8366 3.44609 16.8053 3.30179L17.3009 2.80624C18.3759 1.73125 20.1188 1.73125 21.1938 2.80624Z"
|
||||
/>
|
||||
<path
|
||||
d="M14.5801 13.3128C14.1761 13.7168 13.9741 13.9188 13.7513 14.0926C13.4886 14.2975 13.2043 14.4732 12.9035 14.6166C12.6485 14.7381 12.3775 14.8284 11.8354 15.0091L8.97709 15.9619C8.71035 16.0508 8.41626 15.9814 8.21744 15.7826C8.01862 15.5837 7.9492 15.2897 8.03811 15.0229L8.99089 12.1646C9.17157 11.6225 9.26191 11.3515 9.38344 11.0965C9.52679 10.7957 9.70249 10.5114 9.90743 10.2487C10.0812 10.0259 10.2832 9.82394 10.6872 9.41993L15.6033 4.50385C15.867 5.19804 16.3293 6.05663 17.1363 6.86366C17.9434 7.67069 18.802 8.13296 19.4962 8.39674L14.5801 13.3128Z"
|
||||
/>
|
||||
<path
|
||||
d="M20.5355 20.5355C22 19.0711 22 16.714 22 12C22 10.4517 22 9.15774 21.9481 8.0661L15.586 14.4283C15.2347 14.7797 14.9708 15.0437 14.6738 15.2753C14.3252 15.5473 13.948 15.7804 13.5488 15.9706C13.2088 16.1327 12.8546 16.2506 12.3833 16.4076L9.45143 17.3849C8.64568 17.6535 7.75734 17.4438 7.15678 16.8432C6.55621 16.2427 6.34651 15.3543 6.61509 14.5486L7.59235 11.6167C7.74936 11.1454 7.86732 10.7912 8.02935 10.4512C8.21958 10.052 8.45272 9.6748 8.72466 9.32615C8.9563 9.02918 9.22032 8.76528 9.57173 8.41404L15.9339 2.05188C14.8423 2 13.5483 2 12 2C7.28595 2 4.92893 2 3.46447 3.46447C2 4.92893 2 7.28595 2 12C2 16.714 2 19.0711 3.46447 20.5355C4.92893 22 7.28595 22 12 22C16.714 22 19.0711 22 20.5355 20.5355Z"
|
||||
/>
|
||||
</svg>
|
||||
</label>
|
||||
<input type="checkbox" id="edit-button" class="hidden css-button"/>
|
||||
<form
|
||||
method="POST"
|
||||
action="./{{ .Data.ID }}/edit"
|
||||
class="absolute bottom-7 left-5 text-black bg-gray-200 transition-all duration-200 rounded shadow-inner shadow-lg shadow-gray-500 dark:text-white dark:shadow-gray-900 dark:bg-gray-600 text-sm p-3"
|
||||
>
|
||||
<label class="font-medium" for="isbn">ISBN</label>
|
||||
<input class="mt-1 mb-2 p-1 bg-gray-400 text-black dark:bg-gray-700 dark:text-white" type="text" id="isbn" name="ISBN"><br>
|
||||
<button class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Search Metadata</button>
|
||||
</form>
|
||||
</div>
|
||||
{{ if .Data.Filepath }}
|
||||
<a href="./{{.Data.ID}}/file">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
class="hover:text-gray-800 dark:hover:text-gray-100"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12ZM12 6.25C12.4142 6.25 12.75 6.58579 12.75 7V12.1893L14.4697 10.4697C14.7626 10.1768 15.2374 10.1768 15.5303 10.4697C15.8232 10.7626 15.8232 11.2374 15.5303 11.5303L12.5303 14.5303C12.3897 14.671 12.1989 14.75 12 14.75C11.8011 14.75 11.6103 14.671 11.4697 14.5303L8.46967 11.5303C8.17678 11.2374 8.17678 10.7626 8.46967 10.4697C8.76256 10.1768 9.23744 10.1768 9.53033 10.4697L11.25 12.1893V7C11.25 6.58579 11.5858 6.25 12 6.25ZM8 16.25C7.58579 16.25 7.25 16.5858 7.25 17C7.25 17.4142 7.58579 17.75 8 17.75H16C16.4142 17.75 16.75 17.4142 16.75 17C16.75 16.5858 16.4142 16.25 16 16.25H8Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
{{ else }}
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
class="text-gray-200 dark:text-gray-600"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12ZM12 6.25C12.4142 6.25 12.75 6.58579 12.75 7V12.1893L14.4697 10.4697C14.7626 10.1768 15.2374 10.1768 15.5303 10.4697C15.8232 10.7626 15.8232 11.2374 15.5303 11.5303L12.5303 14.5303C12.3897 14.671 12.1989 14.75 12 14.75C11.8011 14.75 11.6103 14.671 11.4697 14.5303L8.46967 11.5303C8.17678 11.2374 8.17678 10.7626 8.46967 10.4697C8.76256 10.1768 9.23744 10.1768 9.53033 10.4697L11.25 12.1893V7C11.25 6.58579 11.5858 6.25 12 6.25ZM8 16.25C7.58579 16.25 7.25 16.5858 7.25 17C7.25 17.4142 7.58579 17.75 8 17.75H16C16.4142 17.75 16.75 17.4142 16.75 17C16.75 16.5858 16.4142 16.25 16 16.25H8Z"
|
||||
/>
|
||||
</svg>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap justify-between gap-6 pb-6">
|
||||
<div>
|
||||
<p class="text-gray-400">Title</p>
|
||||
<!-- <input type="text" class="font-medium bg-transparent" value="{{ or .Data.Title "Unknown" }}"/> -->
|
||||
<p class="font-medium text-lg">
|
||||
{{ or .Data.Title "N/A" }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-400">Author</p>
|
||||
<p class="font-medium text-lg">
|
||||
{{ or .Data.Author "N/A" }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-400">Progress</p>
|
||||
<p class="font-medium text-lg">
|
||||
{{ .Data.CurrentPage }} / {{ .Data.TotalPages }} ({{ .Data.Percentage }}%)
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-400">Time Read</p>
|
||||
<p class="font-medium text-lg">
|
||||
{{ .Data.TotalTimeMinutes }} Minutes
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-400">Description</p>
|
||||
<p class="font-medium text-justify hyphens-auto">
|
||||
{{ or .Data.Description "N/A" }}
|
||||
</p>
|
||||
</div>
|
||||
<style>
|
||||
.css-button:checked + form {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.css-button + form {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
@ -7,10 +7,37 @@
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="h-full w-full overflow-scroll bg-white shadow-lg dark:bg-gray-700 rounded dark:text-white text-sm p-4">
|
||||
<div class="flex flex-col gap-2 float-left mr-4">
|
||||
<div class="h-full w-full overflow-scroll bg-white shadow-lg dark:bg-gray-700 rounded dark:text-white p-4">
|
||||
<div class="flex flex-col gap-2 float-left mr-4 mb-4 relative">
|
||||
<label class="z-20 cursor-pointer" for="edit-cover-button">
|
||||
<img class="rounded w-40 md:w-60 lg:w-80 object-fill h-full" src="../documents/{{.Data.ID}}/cover"></img>
|
||||
<div class="flex gap-2 justify-end text-gray-500 dark:text-gray-400">
|
||||
</label>
|
||||
<div class="flex relative z-40 gap-2 justify-end text-gray-500 dark:text-gray-400">
|
||||
<input type="checkbox" id="edit-cover-button" class="hidden css-button"/>
|
||||
<div class="absolute z-50 flex flex-col gap-2 top-0 left-0 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
|
||||
<form
|
||||
method="POST"
|
||||
enctype="multipart/form-data"
|
||||
action="./{{ .Data.ID }}/edit"
|
||||
class="flex flex-col gap-2 w-72 text-black dark:text-white text-sm"
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
id="cover"
|
||||
name="cover"
|
||||
>
|
||||
<button class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Upload Cover</button>
|
||||
</form>
|
||||
<form
|
||||
method="POST"
|
||||
action="./{{ .Data.ID }}/edit"
|
||||
class="flex flex-col gap-2 w-72 text-black dark:text-white text-sm"
|
||||
>
|
||||
<input type="checkbox" checked id="remove_cover" name="remove_cover" class="hidden" />
|
||||
<button class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Remove Cover</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
<div class="relative">
|
||||
<label for="delete-button">
|
||||
<svg
|
||||
@ -30,15 +57,17 @@
|
||||
</svg>
|
||||
</label>
|
||||
<input type="checkbox" id="delete-button" class="hidden css-button"/>
|
||||
<div class="absolute z-50 bottom-7 left-5 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
|
||||
<form
|
||||
method="POST"
|
||||
action="./{{ .Data.ID }}/delete"
|
||||
class="absolute bottom-7 left-5 text-black bg-gray-200 transition-all duration-200 rounded shadow-inner shadow-lg shadow-gray-500 dark:text-white dark:shadow-gray-900 dark:bg-gray-600 text-sm p-3"
|
||||
class="text-black dark:text-white text-sm"
|
||||
>
|
||||
<p class="font-medium w-24 pb-2">Are you sure?</p>
|
||||
<button class="font-medium w-full px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<a href="../activity?document={{ .Data.ID }}">
|
||||
<svg
|
||||
width="24"
|
||||
@ -53,7 +82,7 @@
|
||||
</svg>
|
||||
</a>
|
||||
<div class="relative">
|
||||
<label for="metadata-button">
|
||||
<label for="edit-button">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
@ -63,23 +92,34 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22ZM12 7.75C11.3787 7.75 10.875 8.25368 10.875 8.875C10.875 9.28921 10.5392 9.625 10.125 9.625C9.71079 9.625 9.375 9.28921 9.375 8.875C9.375 7.42525 10.5503 6.25 12 6.25C13.4497 6.25 14.625 7.42525 14.625 8.875C14.625 9.58584 14.3415 10.232 13.883 10.704C13.7907 10.7989 13.7027 10.8869 13.6187 10.9708C13.4029 11.1864 13.2138 11.3753 13.0479 11.5885C12.8289 11.8699 12.75 12.0768 12.75 12.25V13C12.75 13.4142 12.4142 13.75 12 13.75C11.5858 13.75 11.25 13.4142 11.25 13V12.25C11.25 11.5948 11.555 11.0644 11.8642 10.6672C12.0929 10.3733 12.3804 10.0863 12.6138 9.85346C12.6842 9.78321 12.7496 9.71789 12.807 9.65877C13.0046 9.45543 13.125 9.18004 13.125 8.875C13.125 8.25368 12.6213 7.75 12 7.75ZM12 17C12.5523 17 13 16.5523 13 16C13 15.4477 12.5523 15 12 15C11.4477 15 11 15.4477 11 16C11 16.5523 11.4477 17 12 17Z"
|
||||
d="M21.1938 2.80624C22.2687 3.88124 22.2687 5.62415 21.1938 6.69914L20.6982 7.19469C20.5539 7.16345 20.3722 7.11589 20.1651 7.04404C19.6108 6.85172 18.8823 6.48827 18.197 5.803C17.5117 5.11774 17.1483 4.38923 16.956 3.8349C16.8841 3.62781 16.8366 3.44609 16.8053 3.30179L17.3009 2.80624C18.3759 1.73125 20.1188 1.73125 21.1938 2.80624Z"
|
||||
/>
|
||||
<path
|
||||
d="M14.5801 13.3128C14.1761 13.7168 13.9741 13.9188 13.7513 14.0926C13.4886 14.2975 13.2043 14.4732 12.9035 14.6166C12.6485 14.7381 12.3775 14.8284 11.8354 15.0091L8.97709 15.9619C8.71035 16.0508 8.41626 15.9814 8.21744 15.7826C8.01862 15.5837 7.9492 15.2897 8.03811 15.0229L8.99089 12.1646C9.17157 11.6225 9.26191 11.3515 9.38344 11.0965C9.52679 10.7957 9.70249 10.5114 9.90743 10.2487C10.0812 10.0259 10.2832 9.82394 10.6872 9.41993L15.6033 4.50385C15.867 5.19804 16.3293 6.05663 17.1363 6.86366C17.9434 7.67069 18.802 8.13296 19.4962 8.39674L14.5801 13.3128Z"
|
||||
/>
|
||||
<path
|
||||
d="M20.5355 20.5355C22 19.0711 22 16.714 22 12C22 10.4517 22 9.15774 21.9481 8.0661L15.586 14.4283C15.2347 14.7797 14.9708 15.0437 14.6738 15.2753C14.3252 15.5473 13.948 15.7804 13.5488 15.9706C13.2088 16.1327 12.8546 16.2506 12.3833 16.4076L9.45143 17.3849C8.64568 17.6535 7.75734 17.4438 7.15678 16.8432C6.55621 16.2427 6.34651 15.3543 6.61509 14.5486L7.59235 11.6167C7.74936 11.1454 7.86732 10.7912 8.02935 10.4512C8.21958 10.052 8.45272 9.6748 8.72466 9.32615C8.9563 9.02918 9.22032 8.76528 9.57173 8.41404L15.9339 2.05188C14.8423 2 13.5483 2 12 2C7.28595 2 4.92893 2 3.46447 3.46447C2 4.92893 2 7.28595 2 12C2 16.714 2 19.0711 3.46447 20.5355C4.92893 22 7.28595 22 12 22C16.714 22 19.0711 22 20.5355 20.5355Z"
|
||||
/>
|
||||
</svg>
|
||||
</label>
|
||||
<input type="checkbox" id="metadata-button" class="hidden css-button"/>
|
||||
<input type="checkbox" id="edit-button" class="hidden css-button"/>
|
||||
<div class="absolute z-50 bottom-7 left-5 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
|
||||
<form
|
||||
method="POST"
|
||||
action="./{{ .Data.ID }}/identify"
|
||||
class="absolute bottom-7 left-5 text-black bg-gray-200 transition-all duration-200 rounded shadow-inner shadow-lg shadow-gray-500 dark:text-white dark:shadow-gray-900 dark:bg-gray-600 text-sm p-3"
|
||||
action="./{{ .Data.ID }}/edit"
|
||||
class="text-black dark:text-white text-sm"
|
||||
>
|
||||
<label class="font-medium" for="isbn">ISBN</label>
|
||||
<input class="mt-1 mb-2 p-1 bg-gray-400 text-black dark:bg-gray-700 dark:text-white" type="text" id="isbn" name="isbn"><br>
|
||||
<input
|
||||
type="text"
|
||||
id="isbn"
|
||||
name="ISBN"
|
||||
class="mt-1 mb-2 p-2 bg-gray-400 text-black dark:bg-gray-700 dark:text-white"
|
||||
><br>
|
||||
<button class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Search Metadata</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{ if .Data.Filepath }}
|
||||
<a href="./{{.Data.ID}}/file">
|
||||
<svg
|
||||
@ -116,44 +156,180 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap justify-between gap-4 pb-4">
|
||||
<div>
|
||||
<p class="text-gray-400">Title</p>
|
||||
<!-- <input type="text" class="font-medium bg-transparent" value="{{ or .Data.Title "Unknown" }}"/> -->
|
||||
<p class="font-medium">
|
||||
<div class="relative">
|
||||
<div class="text-gray-400 inline-flex gap-2 relative">
|
||||
<p>Title</p>
|
||||
<label class="my-auto" for="edit-title-button">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
class="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M21.1938 2.80624C22.2687 3.88124 22.2687 5.62415 21.1938 6.69914L20.6982 7.19469C20.5539 7.16345 20.3722 7.11589 20.1651 7.04404C19.6108 6.85172 18.8823 6.48827 18.197 5.803C17.5117 5.11774 17.1483 4.38923 16.956 3.8349C16.8841 3.62781 16.8366 3.44609 16.8053 3.30179L17.3009 2.80624C18.3759 1.73125 20.1188 1.73125 21.1938 2.80624Z"
|
||||
/>
|
||||
<path
|
||||
d="M14.5801 13.3128C14.1761 13.7168 13.9741 13.9188 13.7513 14.0926C13.4886 14.2975 13.2043 14.4732 12.9035 14.6166C12.6485 14.7381 12.3775 14.8284 11.8354 15.0091L8.97709 15.9619C8.71035 16.0508 8.41626 15.9814 8.21744 15.7826C8.01862 15.5837 7.9492 15.2897 8.03811 15.0229L8.99089 12.1646C9.17157 11.6225 9.26191 11.3515 9.38344 11.0965C9.52679 10.7957 9.70249 10.5114 9.90743 10.2487C10.0812 10.0259 10.2832 9.82394 10.6872 9.41993L15.6033 4.50385C15.867 5.19804 16.3293 6.05663 17.1363 6.86366C17.9434 7.67069 18.802 8.13296 19.4962 8.39674L14.5801 13.3128Z"
|
||||
/>
|
||||
<path
|
||||
d="M20.5355 20.5355C22 19.0711 22 16.714 22 12C22 10.4517 22 9.15774 21.9481 8.0661L15.586 14.4283C15.2347 14.7797 14.9708 15.0437 14.6738 15.2753C14.3252 15.5473 13.948 15.7804 13.5488 15.9706C13.2088 16.1327 12.8546 16.2506 12.3833 16.4076L9.45143 17.3849C8.64568 17.6535 7.75734 17.4438 7.15678 16.8432C6.55621 16.2427 6.34651 15.3543 6.61509 14.5486L7.59235 11.6167C7.74936 11.1454 7.86732 10.7912 8.02935 10.4512C8.21958 10.052 8.45272 9.6748 8.72466 9.32615C8.9563 9.02918 9.22032 8.76528 9.57173 8.41404L15.9339 2.05188C14.8423 2 13.5483 2 12 2C7.28595 2 4.92893 2 3.46447 3.46447C2 4.92893 2 7.28595 2 12C2 16.714 2 19.0711 3.46447 20.5355C4.92893 22 7.28595 22 12 22C16.714 22 19.0711 22 20.5355 20.5355Z"
|
||||
/>
|
||||
</svg>
|
||||
</label>
|
||||
<input type="checkbox" id="edit-title-button" class="hidden css-button"/>
|
||||
<div class="absolute z-50 top-7 right-0 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
|
||||
<form
|
||||
method="POST"
|
||||
action="./{{ .Data.ID }}/edit"
|
||||
class="flex flex-col gap-2 text-black dark:text-white text-sm"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
value="{{ or .Data.Title "N/A" }}"
|
||||
class="p-2 bg-gray-400 text-black dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
<button class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<p class="font-medium text-lg">
|
||||
{{ or .Data.Title "N/A" }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-400">Author</p>
|
||||
<p class="font-medium">
|
||||
<div class="relative">
|
||||
<div class="text-gray-400 inline-flex gap-2 relative">
|
||||
<p>Author</p>
|
||||
<label class="my-auto" for="edit-author-button">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
class="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M21.1938 2.80624C22.2687 3.88124 22.2687 5.62415 21.1938 6.69914L20.6982 7.19469C20.5539 7.16345 20.3722 7.11589 20.1651 7.04404C19.6108 6.85172 18.8823 6.48827 18.197 5.803C17.5117 5.11774 17.1483 4.38923 16.956 3.8349C16.8841 3.62781 16.8366 3.44609 16.8053 3.30179L17.3009 2.80624C18.3759 1.73125 20.1188 1.73125 21.1938 2.80624Z"
|
||||
/>
|
||||
<path
|
||||
d="M14.5801 13.3128C14.1761 13.7168 13.9741 13.9188 13.7513 14.0926C13.4886 14.2975 13.2043 14.4732 12.9035 14.6166C12.6485 14.7381 12.3775 14.8284 11.8354 15.0091L8.97709 15.9619C8.71035 16.0508 8.41626 15.9814 8.21744 15.7826C8.01862 15.5837 7.9492 15.2897 8.03811 15.0229L8.99089 12.1646C9.17157 11.6225 9.26191 11.3515 9.38344 11.0965C9.52679 10.7957 9.70249 10.5114 9.90743 10.2487C10.0812 10.0259 10.2832 9.82394 10.6872 9.41993L15.6033 4.50385C15.867 5.19804 16.3293 6.05663 17.1363 6.86366C17.9434 7.67069 18.802 8.13296 19.4962 8.39674L14.5801 13.3128Z"
|
||||
/>
|
||||
<path
|
||||
d="M20.5355 20.5355C22 19.0711 22 16.714 22 12C22 10.4517 22 9.15774 21.9481 8.0661L15.586 14.4283C15.2347 14.7797 14.9708 15.0437 14.6738 15.2753C14.3252 15.5473 13.948 15.7804 13.5488 15.9706C13.2088 16.1327 12.8546 16.2506 12.3833 16.4076L9.45143 17.3849C8.64568 17.6535 7.75734 17.4438 7.15678 16.8432C6.55621 16.2427 6.34651 15.3543 6.61509 14.5486L7.59235 11.6167C7.74936 11.1454 7.86732 10.7912 8.02935 10.4512C8.21958 10.052 8.45272 9.6748 8.72466 9.32615C8.9563 9.02918 9.22032 8.76528 9.57173 8.41404L15.9339 2.05188C14.8423 2 13.5483 2 12 2C7.28595 2 4.92893 2 3.46447 3.46447C2 4.92893 2 7.28595 2 12C2 16.714 2 19.0711 3.46447 20.5355C4.92893 22 7.28595 22 12 22C16.714 22 19.0711 22 20.5355 20.5355Z"
|
||||
/>
|
||||
</svg>
|
||||
</label>
|
||||
<input type="checkbox" id="edit-author-button" class="hidden css-button"/>
|
||||
|
||||
<div class="absolute z-50 top-7 right-0 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
|
||||
<form
|
||||
method="POST"
|
||||
action="./{{ .Data.ID }}/edit"
|
||||
class="flex flex-col gap-2 text-black dark:text-white text-sm"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="author"
|
||||
name="author"
|
||||
value="{{ or .Data.Author "N/A" }}"
|
||||
class="p-2 bg-gray-400 text-black dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
<button class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<p class="font-medium text-lg">
|
||||
{{ or .Data.Author "N/A" }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-400">Progress</p>
|
||||
<p class="font-medium">
|
||||
{{ .Data.CurrentPage }} / {{ .Data.TotalPages }} ({{ .Data.Percentage }}%)
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-400">Minutes Read</p>
|
||||
<p class="font-medium">
|
||||
<p class="text-gray-400">Time Read</p>
|
||||
<p class="font-medium text-lg">
|
||||
{{ .Data.TotalTimeMinutes }} Minutes
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-400">Description</p>
|
||||
<p class="font-medium text-justify hyphens-auto">
|
||||
{{ or .Data.Description "N/A" }}
|
||||
<div>
|
||||
<p class="text-gray-400">Progress</p>
|
||||
<p class="font-medium text-lg">
|
||||
{{ .Data.CurrentPage }} / {{ .Data.TotalPages }} ({{ .Data.Percentage }}%)
|
||||
</p>
|
||||
</div>
|
||||
<!--
|
||||
<div>
|
||||
<p class="text-gray-400">ISBN 10</p>
|
||||
<p class="font-medium text-lg">
|
||||
{{ or .Data.Isbn10 "N/A" }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-400">ISBN 13</p>
|
||||
<p class="font-medium text-lg">
|
||||
{{ or .Data.Isbn13 "N/A" }}
|
||||
</p>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<div class="text-gray-400 inline-flex gap-2 relative">
|
||||
<p>Description</p>
|
||||
<label class="my-auto" for="edit-description-button">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
class="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M21.1938 2.80624C22.2687 3.88124 22.2687 5.62415 21.1938 6.69914L20.6982 7.19469C20.5539 7.16345 20.3722 7.11589 20.1651 7.04404C19.6108 6.85172 18.8823 6.48827 18.197 5.803C17.5117 5.11774 17.1483 4.38923 16.956 3.8349C16.8841 3.62781 16.8366 3.44609 16.8053 3.30179L17.3009 2.80624C18.3759 1.73125 20.1188 1.73125 21.1938 2.80624Z"
|
||||
/>
|
||||
<path
|
||||
d="M14.5801 13.3128C14.1761 13.7168 13.9741 13.9188 13.7513 14.0926C13.4886 14.2975 13.2043 14.4732 12.9035 14.6166C12.6485 14.7381 12.3775 14.8284 11.8354 15.0091L8.97709 15.9619C8.71035 16.0508 8.41626 15.9814 8.21744 15.7826C8.01862 15.5837 7.9492 15.2897 8.03811 15.0229L8.99089 12.1646C9.17157 11.6225 9.26191 11.3515 9.38344 11.0965C9.52679 10.7957 9.70249 10.5114 9.90743 10.2487C10.0812 10.0259 10.2832 9.82394 10.6872 9.41993L15.6033 4.50385C15.867 5.19804 16.3293 6.05663 17.1363 6.86366C17.9434 7.67069 18.802 8.13296 19.4962 8.39674L14.5801 13.3128Z"
|
||||
/>
|
||||
<path
|
||||
d="M20.5355 20.5355C22 19.0711 22 16.714 22 12C22 10.4517 22 9.15774 21.9481 8.0661L15.586 14.4283C15.2347 14.7797 14.9708 15.0437 14.6738 15.2753C14.3252 15.5473 13.948 15.7804 13.5488 15.9706C13.2088 16.1327 12.8546 16.2506 12.3833 16.4076L9.45143 17.3849C8.64568 17.6535 7.75734 17.4438 7.15678 16.8432C6.55621 16.2427 6.34651 15.3543 6.61509 14.5486L7.59235 11.6167C7.74936 11.1454 7.86732 10.7912 8.02935 10.4512C8.21958 10.052 8.45272 9.6748 8.72466 9.32615C8.9563 9.02918 9.22032 8.76528 9.57173 8.41404L15.9339 2.05188C14.8423 2 13.5483 2 12 2C7.28595 2 4.92893 2 3.46447 3.46447C2 4.92893 2 7.28595 2 12C2 16.714 2 19.0711 3.46447 20.5355C4.92893 22 7.28595 22 12 22C16.714 22 19.0711 22 20.5355 20.5355Z"
|
||||
/>
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative font-medium text-justify hyphens-auto">
|
||||
<input type="checkbox" id="edit-description-button" class="hidden css-button"/>
|
||||
<div
|
||||
class="absolute h-full w-full min-h-[10em] z-50 top-1 right-0 gap-6 flex transition-all duration-200"
|
||||
>
|
||||
<img class="hidden md:block invisible rounded w-40 md:w-60 lg:w-80 object-fill" src="../documents/{{.Data.ID}}/cover"></img>
|
||||
<form
|
||||
method="POST"
|
||||
action="./{{ .Data.ID }}/edit"
|
||||
class="flex flex-col gap-2 w-full text-black bg-gray-200 rounded shadow-lg shadow-gray-500 dark:text-white dark:shadow-gray-900 dark:bg-gray-600 text-sm p-3"
|
||||
>
|
||||
<textarea
|
||||
type="text"
|
||||
id="description"
|
||||
name="description"
|
||||
class="h-full w-full p-2 bg-gray-400 text-black dark:bg-gray-700 dark:text-white"
|
||||
>{{ or .Data.Description "N/A" }}</textarea>
|
||||
<button class="font-medium px-2 py-1 text-white bg-gray-500 dark:text-gray-800 dark:bg-gray-400 hover:bg-gray-800 dark:hover:bg-gray-100" type="submit">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
<p>{{ or .Data.Description "N/A" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.css-button:checked + form {
|
||||
.css-button:checked + div {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.css-button + form {
|
||||
.css-button + div {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
@ -43,7 +43,7 @@
|
||||
</div>
|
||||
<div class="inline-flex shrink-0 items-center">
|
||||
<div>
|
||||
<p class="text-gray-400">Minutes Read</p>
|
||||
<p class="text-gray-400">Time Read</p>
|
||||
<p class="font-medium">
|
||||
{{ $doc.TotalTimeMinutes }} Minutes
|
||||
</p>
|
||||
|
Loading…
Reference in New Issue
Block a user