[add] search

This commit is contained in:
Evan Reichard 2023-10-06 21:25:56 -04:00
parent 70c7f4b991
commit edca763396
11 changed files with 681 additions and 6 deletions

View File

@ -82,6 +82,7 @@ func (api *API) registerWebAppRoutes() {
render.AddFromFilesFuncs("login", helperFuncs, "templates/login.html")
render.AddFromFilesFuncs("home", helperFuncs, "templates/base.html", "templates/home.html")
render.AddFromFilesFuncs("search", helperFuncs, "templates/base.html", "templates/search.html")
render.AddFromFilesFuncs("settings", helperFuncs, "templates/base.html", "templates/settings.html")
render.AddFromFilesFuncs("activity", helperFuncs, "templates/base.html", "templates/activity.html")
render.AddFromFilesFuncs("documents", helperFuncs, "templates/base.html", "templates/documents.html")
@ -107,6 +108,12 @@ func (api *API) registerWebAppRoutes() {
api.Router.POST("/documents/:document/edit", api.authWebAppMiddleware, api.editDocument)
api.Router.POST("/documents/:document/identify", api.authWebAppMiddleware, api.identifyDocument)
api.Router.POST("/documents/:document/delete", api.authWebAppMiddleware, api.deleteDocument)
// Behind Configuration Flag
if api.Config.SearchEnabled {
api.Router.GET("/search", api.authWebAppMiddleware, api.createAppResourcesRoute("search"))
api.Router.POST("/search", api.authWebAppMiddleware, api.saveNewDocument)
}
}
func (api *API) registerKOAPIRoutes(apiGroup *gin.RouterGroup) {

View File

@ -17,6 +17,8 @@ import (
"golang.org/x/exp/slices"
"reichard.io/bbank/database"
"reichard.io/bbank/metadata"
"reichard.io/bbank/search"
"reichard.io/bbank/utils"
)
type queryParams struct {
@ -25,6 +27,11 @@ type queryParams struct {
Document *string `form:"document"`
}
type searchParams struct {
Query *string `form:"query"`
BookType *string `form:"book_type"`
}
type requestDocumentEdit struct {
Title *string `form:"title"`
Author *string `form:"author"`
@ -48,6 +55,13 @@ type requestSettingsEdit struct {
TimeOffset *string `form:"time_offset"`
}
type requestDocumentAdd struct {
ID *string `form:"id"`
Title *string `form:"title"`
Author *string `form:"author"`
BookType *string `form:"book_type"`
}
func (api *API) webManifest(c *gin.Context) {
c.Header("Content-Type", "application/manifest+json")
c.File("./assets/manifest.json")
@ -60,6 +74,7 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
templateVarsBase = args[0]
}
templateVarsBase["RouteName"] = routeName
templateVarsBase["SearchEnabled"] = api.Config.SearchEnabled
return func(c *gin.Context) {
var userID string
@ -174,6 +189,26 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
},
"Devices": devices,
}
} else if routeName == "search" {
var sParams searchParams
c.BindQuery(&sParams)
// Only Handle Query
if sParams.BookType != nil && !slices.Contains([]string{"NON_FICTION", "FICTION"}, *sParams.BookType) {
templateVars["SearchErrorMessage"] = "Invalid Book Type"
} else if sParams.Query != nil && *sParams.Query == "" {
templateVars["SearchErrorMessage"] = "Invalid Query"
} else if sParams.BookType != nil && sParams.Query != nil {
var bType search.BookType = search.BOOK_FICTION
if *sParams.BookType == "NON_FICTION" {
bType = search.BOOK_NON_FICTION
}
// Search
searchResults := search.SearchBook(*sParams.Query, bType)
templateVars["Data"] = searchResults
templateVars["BookType"] = *sParams.BookType
}
} else if routeName == "login" {
templateVars["RegistrationEnabled"] = api.Config.RegistrationEnabled
}
@ -431,6 +466,7 @@ func (api *API) identifyDocument(c *gin.Context) {
// Template Variables
templateVars := gin.H{
"RelBase": "../../",
"SearchEnabled": api.Config.SearchEnabled,
}
// Get Metadata
@ -479,6 +515,103 @@ func (api *API) identifyDocument(c *gin.Context) {
c.HTML(http.StatusOK, "document", templateVars)
}
func (api *API) saveNewDocument(c *gin.Context) {
var rDocAdd requestDocumentAdd
if err := c.ShouldBind(&rDocAdd); err != nil {
log.Error("[saveNewDocument] Invalid Form Bind")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
return
}
// Validate Form Exists
if rDocAdd.ID == nil ||
rDocAdd.BookType == nil ||
rDocAdd.Title == nil ||
rDocAdd.Author == nil {
log.Error("[saveNewDocument] Missing Form Values")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
return
}
var bType search.BookType = search.BOOK_FICTION
if *rDocAdd.BookType == "NON_FICTION" {
bType = search.BOOK_NON_FICTION
}
// Save Book
tempFilePath, err := search.SaveBook(*rDocAdd.ID, bType)
if err != nil {
log.Warn("[saveNewDocument] Temp File Error: ", err)
c.AbortWithStatus(http.StatusBadRequest)
return
}
// Calculate Partial MD5 ID
partialMD5, err := utils.CalculatePartialMD5(tempFilePath)
if err != nil {
log.Warn("[saveNewDocument] Partial MD5 Error: ", err)
c.AbortWithStatus(http.StatusBadRequest)
return
}
// Derive Extension on MIME
fileMime, err := mimetype.DetectFile(tempFilePath)
fileExtension := fileMime.Extension()
// Derive Filename
var fileName string
if *rDocAdd.Author != "" {
fileName = fileName + *rDocAdd.Author
} else {
fileName = fileName + "Unknown"
}
if *rDocAdd.Title != "" {
fileName = fileName + " - " + *rDocAdd.Title
} else {
fileName = fileName + " - Unknown"
}
// Remove Slashes
fileName = strings.ReplaceAll(fileName, "/", "")
// Derive & Sanitize File Name
fileName = "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, partialMD5, fileExtension))
// Generate Storage Path
safePath := filepath.Join(api.Config.DataPath, "documents", fileName)
// Move File
if err := os.Rename(tempFilePath, safePath); err != nil {
log.Warn("[saveNewDocument] Move Temp File Error: ", err)
c.AbortWithStatus(http.StatusBadRequest)
return
}
// Get MD5 Hash
fileHash, err := getFileMD5(safePath)
if err != nil {
log.Error("[saveNewDocument] Hash Failure:", err)
c.AbortWithStatus(http.StatusBadRequest)
return
}
// Upsert Document
if _, err = api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
ID: partialMD5,
Title: rDocAdd.Title,
Author: rDocAdd.Author,
Md5: fileHash,
Filepath: &fileName,
}); err != nil {
log.Error("[saveNewDocument] UpsertDocument DB Error:", err)
c.AbortWithStatus(http.StatusBadRequest)
return
}
c.Redirect(http.StatusFound, fmt.Sprintf("./documents/%s", partialMD5))
}
func (api *API) editSettings(c *gin.Context) {
rUser, _ := c.Get("AuthorizedUser")
@ -556,6 +689,7 @@ func (api *API) editSettings(c *gin.Context) {
"TimeOffset": *user.TimeOffset,
},
"Devices": devices,
"SearchEnabled": api.Config.SearchEnabled,
}
c.HTML(http.StatusOK, "settings", templateVars)

View File

@ -22,6 +22,7 @@ type Config struct {
// Miscellaneous Settings
RegistrationEnabled bool
CookieSessionKey string
SearchEnabled bool
}
func Load() *Config {
@ -35,6 +36,7 @@ func Load() *Config {
ListenPort: getEnv("LISTEN_PORT", "8585"),
CookieSessionKey: trimLowerString(getEnv("COOKIE_SESSION_KEY", "")),
RegistrationEnabled: trimLowerString(getEnv("REGISTRATION_ENABLED", "false")) == "true",
SearchEnabled: trimLowerString(getEnv("SEARCH_ENABLED", "false")) == "true",
}
}

2
go.mod
View File

@ -17,6 +17,8 @@ require (
)
require (
github.com/PuerkitoBio/goquery v1.8.1 // indirect
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/bytedance/sonic v1.10.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect

8
go.sum
View File

@ -1,5 +1,9 @@
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/alexedwards/argon2id v0.0.0-20230305115115-4b3c3280a736 h1:qZaEtLxnqY5mJ0fVKbk31NVhlgi0yrKm51Pq/I5wcz4=
github.com/alexedwards/argon2id v0.0.0-20230305115115-4b3c3280a736/go.mod h1:mTeFRcTdnpzOlRjMoFYC/80HwVUreupyAiqPkCZQOXc=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/antonlindstrom/pgstore v0.0.0-20200229204646-b08ebf1105e0/go.mod h1:2Ti6VUHVxpC0VSmTZzEvpzysnaGAfGBOoMIz5ykPyyw=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
@ -174,8 +178,10 @@ golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
@ -187,6 +193,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -218,6 +225,7 @@ golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=

253
search/search.go Normal file
View File

@ -0,0 +1,253 @@
package search
import (
"errors"
"io"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
log "github.com/sirupsen/logrus"
)
type Cadence string
const (
TOP_YEAR Cadence = "y"
TOP_MONTH Cadence = "m"
)
type BookType int
const (
BOOK_FICTION BookType = iota
BOOK_NON_FICTION
)
type SearchItem struct {
ID string
Title string
Author string
Language string
Series string
FileType string
FileSize string
UploadDate string
}
func SearchBook(query string, bookType BookType) (allEntries []SearchItem) {
log.Info(query)
if bookType == BOOK_FICTION {
// Search Fiction
url := "https://libgen.is/fiction/?q=" + url.QueryEscape(query) + "&language=English&format=epub"
body := getPage(url)
allEntries = parseLibGenFiction(body)
} else if bookType == BOOK_NON_FICTION {
// Search NonFiction
url := "https://libgen.is/search.php?req=" + url.QueryEscape(query)
body := getPage(url)
allEntries = parseLibGenNonFiction(body)
}
return
}
func GoodReadsMostRead(c Cadence) []SearchItem {
body := getPage("https://www.goodreads.com/book/most_read?category=all&country=US&duration=" + string(c))
return parseGoodReads(body)
}
func GetBookURL(id string, bookType BookType) string {
// Derive Info URL
var infoURL string
if bookType == BOOK_FICTION {
infoURL = "http://library.lol/fiction/" + id
} else if bookType == BOOK_NON_FICTION {
infoURL = "http://library.lol/main/" + id
}
// Parse & Derive Download URL
body := getPage(infoURL)
// downloadURL := parseLibGenDownloadURL(body)
return parseLibGenDownloadURL(body)
}
func SaveBook(id string, bookType BookType) (string, error) {
// Derive Info URL
var infoURL string
if bookType == BOOK_FICTION {
infoURL = "http://library.lol/fiction/" + id
} else if bookType == BOOK_NON_FICTION {
infoURL = "http://library.lol/main/" + id
}
// Parse & Derive Download URL
body := getPage(infoURL)
bookURL := parseLibGenDownloadURL(body)
// Create File
tempFile, err := os.CreateTemp("", "book")
if err != nil {
log.Error("[SaveBook] File Create Error: ", err)
return "", errors.New("File Failure")
}
defer tempFile.Close()
// Download File
log.Info("[SaveBook] Downloading Book")
resp, err := http.Get(bookURL)
if err != nil {
os.Remove(tempFile.Name())
log.Error("[SaveBook] Cover URL API Failure")
return "", errors.New("API Failure")
}
defer resp.Body.Close()
// Copy File to Disk
log.Info("[SaveBook] Saving Book")
_, err = io.Copy(tempFile, resp.Body)
if err != nil {
os.Remove(tempFile.Name())
log.Error("[SaveBook] File Copy Error")
return "", errors.New("File Failure")
}
return tempFile.Name(), nil
}
func getPage(page string) io.ReadCloser {
resp, _ := http.Get(page)
return resp.Body
}
func parseLibGenFiction(body io.ReadCloser) []SearchItem {
// Parse
defer body.Close()
doc, _ := goquery.NewDocumentFromReader(body)
// Normalize Results
var allEntries []SearchItem
doc.Find("table.catalog tbody > tr").Each(func(ix int, rawBook *goquery.Selection) {
// Parse File Details
fileItem := rawBook.Find("td:nth-child(5)")
fileDesc := fileItem.Text()
fileDescSplit := strings.Split(fileDesc, "/")
fileType := strings.ToLower(strings.TrimSpace(fileDescSplit[0]))
fileSize := strings.TrimSpace(fileDescSplit[1])
// Parse Upload Date
uploadedRaw, _ := fileItem.Attr("title")
uploadedDateRaw := strings.Split(uploadedRaw, "Uploaded at ")[1]
uploadDate, _ := time.Parse("2006-01-02 15:04:05", uploadedDateRaw)
// Parse MD5
editHref, _ := rawBook.Find("td:nth-child(7) a").Attr("href")
hrefArray := strings.Split(editHref, "/")
id := hrefArray[len(hrefArray)-1]
// Parse Other Details
title := rawBook.Find("td:nth-child(3) p a").Text()
author := rawBook.Find(".catalog_authors li a").Text()
language := rawBook.Find("td:nth-child(4)").Text()
series := rawBook.Find("td:nth-child(2)").Text()
item := SearchItem{
ID: id,
Title: title,
Author: author,
Series: series,
Language: language,
FileType: fileType,
FileSize: fileSize,
UploadDate: uploadDate.Format(time.RFC3339),
}
allEntries = append(allEntries, item)
})
// Return Results
return allEntries
}
func parseLibGenNonFiction(body io.ReadCloser) []SearchItem {
// Parse
defer body.Close()
doc, _ := goquery.NewDocumentFromReader(body)
// Normalize Results
var allEntries []SearchItem
doc.Find("table.c tbody > tr:nth-child(n + 2)").Each(func(ix int, rawBook *goquery.Selection) {
// Parse Type & Size
fileSize := strings.ToLower(strings.TrimSpace(rawBook.Find("td:nth-child(8)").Text()))
fileType := strings.ToLower(strings.TrimSpace(rawBook.Find("td:nth-child(9)").Text()))
// Parse MD5
titleRaw := rawBook.Find("td:nth-child(3) [id]")
editHref, _ := titleRaw.Attr("href")
hrefArray := strings.Split(editHref, "?md5=")
id := hrefArray[1]
// Parse Other Details
title := titleRaw.Text()
author := rawBook.Find("td:nth-child(2)").Text()
language := rawBook.Find("td:nth-child(7)").Text()
series := rawBook.Find("td:nth-child(3) [href*='column=series']").Text()
item := SearchItem{
ID: id,
Title: title,
Author: author,
Series: series,
Language: language,
FileType: fileType,
FileSize: fileSize,
}
allEntries = append(allEntries, item)
})
// Return Results
return allEntries
}
func parseLibGenDownloadURL(body io.ReadCloser) string {
// Parse
defer body.Close()
doc, _ := goquery.NewDocumentFromReader(body)
// Return Download URL
// downloadURL, _ := doc.Find("#download [href*=cloudflare]").Attr("href")
downloadURL, _ := doc.Find("#download h2 a").Attr("href")
return downloadURL
}
func parseGoodReads(body io.ReadCloser) []SearchItem {
// Parse
defer body.Close()
doc, _ := goquery.NewDocumentFromReader(body)
// Normalize Results
var allEntries []SearchItem
doc.Find("[itemtype=\"http://schema.org/Book\"]").Each(func(ix int, rawBook *goquery.Selection) {
title := rawBook.Find(".bookTitle span").Text()
author := rawBook.Find(".authorName span").Text()
item := SearchItem{
Title: title,
Author: author,
}
allEntries = append(allEntries, item)
})
// Return Results
return allEntries
}

View File

@ -33,6 +33,12 @@
</tr>
</thead>
<tbody class="text-black dark:text-white">
{{ if not .Data }}
<tr>
<td class="text-center p-3" colspan="4">No Results</td>
</tr>
{{ end }}
{{range $activity := .Data }}
<tr>
<td class="p-3 border-b border-gray-200">

View File

@ -79,6 +79,29 @@
</span>
<span class="mx-4 text-sm font-normal">Activity</span>
</a>
{{ if .SearchEnabled }}
<a
class="flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4 {{if eq .RouteName "search"}}border-purple-500 dark:text-white{{else}}border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100{{end}}"
href="/search"
>
<span class="text-left">
<svg
width="20"
height="20"
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 12ZM9 11.5C9 10.1193 10.1193 9 11.5 9C12.8807 9 14 10.1193 14 11.5C14 12.8807 12.8807 14 11.5 14C10.1193 14 9 12.8807 9 11.5ZM11.5 7C9.01472 7 7 9.01472 7 11.5C7 13.9853 9.01472 16 11.5 16C12.3805 16 13.202 15.7471 13.8957 15.31L15.2929 16.7071C15.6834 17.0976 16.3166 17.0976 16.7071 16.7071C17.0976 16.3166 17.0976 15.6834 16.7071 15.2929L15.31 13.8957C15.7471 13.202 16 12.3805 16 11.5C16 9.01472 13.9853 7 11.5 7Z"
></path>
</svg>
</span>
<span class="mx-4 text-sm font-normal">Search</span>
</a>
{{ end }}
</div>
</div>
</div>

197
templates/search.html Normal file
View File

@ -0,0 +1,197 @@
{{template "base.html" .}} {{define "title"}}Search{{end}} {{define "header"}}
<a href="./search">Search</a>
{{end}} {{define "content"}}
<div class="w-full flex flex-col md:flex-row gap-4">
<div class="flex flex-col gap-4 grow">
<div
class="flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
>
<form class="flex gap-4 flex-col lg:flex-row" action="./search">
<div class="flex flex-col w-full grow">
<div class="flex relative">
<span
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
>
<svg
width="15"
height="15"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="24" height="24" fill="none" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18C11.8487 18 13.551 17.3729 14.9056 16.3199L20.2929 21.7071C20.6834 22.0976 21.3166 22.0976 21.7071 21.7071C22.0976 21.3166 22.0976 20.6834 21.7071 20.2929L16.3199 14.9056C17.3729 13.551 18 11.8487 18 10C18 5.58172 14.4183 2 10 2Z"
/>
</svg>
</span>
<input
type="text"
id="query"
name="query"
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
placeholder="Query"
/>
</div>
</div>
<div class="flex relative min-w-[12em]">
<span
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
>
<svg
width="15"
height="15"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.65517 2.22732C5.2225 2.34037 4.9438 2.50021 4.72718 2.71244C4.42179 3.01165 4.22268 3.43172 4.11382 4.225C4.00176 5.04159 4 6.12387 4 7.67568V16.2442C4.38867 15.9781 4.82674 15.7756 5.29899 15.6517C5.41296 15.6217 5.53103 15.5983 5.65517 15.5799V2.22732Z"
/>
<path
d="M7.31034 15.5135C7.32206 15.5135 7.33382 15.5135 7.34563 15.5135L20 15.5135V7.67568C20 6.12387 19.9982 5.04159 19.8862 4.22499C19.7773 3.43172 19.5782 3.01165 19.2728 2.71244C18.9674 2.41324 18.5387 2.21816 17.729 2.11151C16.8955 2.00172 15.7908 2 14.2069 2H9.7931C8.79138 2 7.98133 2.00069 7.31034 2.02897V15.5135Z"
/>
<path
d="M7.47341 17.1351C6.39395 17.1351 6.01657 17.1421 5.72738 17.218C4.93365 17.4264 4.30088 18.0044 4.02952 18.7558C4.0463 19.1382 4.07259 19.4746 4.11382 19.775C4.22268 20.5683 4.42179 20.9884 4.72718 21.2876C5.03258 21.5868 5.46135 21.7818 6.27103 21.8885C7.10452 21.9983 8.2092 22 9.7931 22H14.2069C15.7908 22 16.8955 21.9983 17.729 21.8885C18.5387 21.7818 18.9674 21.5868 19.2728 21.2876C19.5782 20.9884 19.7773 20.5683 19.8862 19.775C19.9776 19.1088 19.9956 18.2657 19.9991 17.1351H7.47341Z"
/>
</svg>
</span>
<select
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
id="book_type"
name="book_type"
>
<option value="FICTION">Fiction</option>
<option value="NON_FICTION">Non-Fiction</option>
</select>
</div>
<button
type="submit"
class="px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2"
>
<span class="w-full">Search</span>
</button>
</form>
{{ if .SearchErrorMessage }}
<span class="text-red-400 text-xs">{{ .SearchErrorMessage }}</span>
{{ end }}
</div>
<div class="inline-block min-w-full overflow-hidden rounded shadow">
<table
class="min-w-full leading-normal bg-white dark:bg-gray-700 text-sm md:text-sm"
>
<thead class="text-gray-800 dark:text-gray-400">
<tr>
<th
scope="col"
class="w-12 p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
></th>
<th
scope="col"
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
>
Document
</th>
<th
scope="col"
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
>
Series
</th>
<th
scope="col"
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
>
Type
</th>
<th
scope="col"
class="p-3 font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
>
Size
</th>
<th
scope="col"
class="p-3 hidden md:block font-normal text-left uppercase border-b border-gray-200 dark:border-gray-800"
>
Date
</th>
</tr>
</thead>
<tbody class="text-black dark:text-white">
{{ if not .Data }}
<tr>
<td class="text-center p-3" colspan="6">No Results</td>
</tr>
{{ end }} {{range $item := .Data }}
<tr>
<td
class="p-3 border-b border-gray-200 text-gray-500 dark:text-gray-500"
>
<form action="./search" method="POST">
<input
class="hidden"
type="text"
id="book_type"
name="book_type"
value="{{ $.BookType }}"
/>
<input
class="hidden"
type="text"
id="title"
name="title"
value="{{ $item.Title }}"
/>
<input
class="hidden"
type="text"
id="author"
name="author"
value="{{ $item.Author }}"
/>
<button name="id" value="{{ $item.ID }}">
<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
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"
></path>
</svg>
</button>
</form>
</td>
<td class="p-3 border-b border-gray-200">
{{ $item.Author }} - {{ $item.Title }}
</td>
<td class="p-3 border-b border-gray-200">
<p>{{ or $item.Series "N/A" }}</p>
</td>
<td class="p-3 border-b border-gray-200">
<p>{{ or $item.FileType "N/A" }}</p>
</td>
<td class="p-3 border-b border-gray-200">
<p>{{ or $item.FileSize "N/A" }}</p>
</td>
<td class="hidden md:table-cell p-3 border-b border-gray-200">
<p>{{ or $item.UploadDate "N/A" }}</p>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
</div>
{{end}}

View File

@ -195,7 +195,11 @@
</tr>
</thead>
<tbody class="text-black dark:text-white">
{{ range $device := .Data.Devices }}
{{ if not .Data.Devices }}
<tr>
<td class="text-center p-3" colspan="3">No Results</td>
</tr>
{{ end }} {{ range $device := .Data.Devices }}
<tr>
<td class="p-3 pl-0">
<p>{{ $device.DeviceName }}</p>

View File

@ -1,8 +1,12 @@
package utils
import (
"bytes"
"crypto/md5"
"fmt"
"io"
"math"
"os"
)
type UTCOffset struct {
@ -82,3 +86,38 @@ func NiceSeconds(input int64) (result string) {
return
}
// Reimplemented KOReader Partial MD5 Calculation
func CalculatePartialMD5(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
var step int64 = 1024
var size int64 = 1024
var buf bytes.Buffer
for i := -1; i <= 10; i++ {
byteStep := make([]byte, size)
var newShift int64 = int64(i * 2)
var newOffset int64
if i == -1 {
newOffset = 0
} else {
newOffset = step << newShift
}
_, err := file.ReadAt(byteStep, newOffset)
if err == io.EOF {
break
}
buf.Write(byteStep)
}
allBytes := buf.Bytes()
return fmt.Sprintf("%x", md5.Sum(allBytes)), nil
}