[add] opds search, [fix] opds urls, [add] log level env var
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
c3410b7833
commit
ca1cce1ff1
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
|
TODO.md
|
||||||
.DS_Store
|
.DS_Store
|
||||||
data/
|
data/
|
||||||
build/
|
build/
|
||||||
|
@ -88,6 +88,7 @@ The service is now accessible at: `http://localhost:8585`. I recommend registeri
|
|||||||
| CONFIG_PATH | /config | Directory where to store SQLite's DB |
|
| CONFIG_PATH | /config | Directory where to store SQLite's DB |
|
||||||
| DATA_PATH | /data | Directory where to store the documents and cover metadata |
|
| DATA_PATH | /data | Directory where to store the documents and cover metadata |
|
||||||
| LISTEN_PORT | 8585 | Port the server listens at |
|
| LISTEN_PORT | 8585 | Port the server listens at |
|
||||||
|
| LOG_LEVEL | info | Set server log level |
|
||||||
| REGISTRATION_ENABLED | false | Whether to allow registration (applies to both WebApp & KOSync API) |
|
| REGISTRATION_ENABLED | false | Whether to allow registration (applies to both WebApp & KOSync API) |
|
||||||
| COOKIE_SESSION_KEY | <EMPTY> | Optional secret cookie session key (auto generated if not provided) |
|
| COOKIE_SESSION_KEY | <EMPTY> | Optional secret cookie session key (auto generated if not provided) |
|
||||||
| COOKIE_SECURE | true | Set Cookie `Secure` attribute (i.e. only works over HTTPS) |
|
| COOKIE_SECURE | true | Set Cookie `Secure` attribute (i.e. only works over HTTPS) |
|
||||||
|
@ -163,11 +163,12 @@ func (api *API) registerOPDSRoutes(apiGroup *gin.RouterGroup) {
|
|||||||
opdsGroup := apiGroup.Group("/opds")
|
opdsGroup := apiGroup.Group("/opds")
|
||||||
|
|
||||||
// OPDS Routes
|
// OPDS Routes
|
||||||
opdsGroup.GET("", api.authOPDSMiddleware, api.opdsDocuments)
|
opdsGroup.GET("", api.authOPDSMiddleware, api.opdsEntry)
|
||||||
opdsGroup.GET("/", api.authOPDSMiddleware, api.opdsDocuments)
|
opdsGroup.GET("/", api.authOPDSMiddleware, api.opdsEntry)
|
||||||
|
opdsGroup.GET("/search.xml", api.authOPDSMiddleware, api.opdsSearchDescription)
|
||||||
|
opdsGroup.GET("/documents", api.authOPDSMiddleware, api.opdsDocuments)
|
||||||
opdsGroup.GET("/documents/:document/cover", api.authOPDSMiddleware, api.getDocumentCover)
|
opdsGroup.GET("/documents/:document/cover", api.authOPDSMiddleware, api.getDocumentCover)
|
||||||
opdsGroup.GET("/documents/:document/file", api.authOPDSMiddleware, api.downloadDocument)
|
opdsGroup.GET("/documents/:document/file", api.authOPDSMiddleware, api.downloadDocument)
|
||||||
opdsGroup.GET("/search.xml", api.authOPDSMiddleware, api.opdsSearchDescription)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateToken(n int) ([]byte, error) {
|
func generateToken(n int) ([]byte, error) {
|
||||||
|
@ -32,8 +32,8 @@ type queryParams struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type searchParams struct {
|
type searchParams struct {
|
||||||
Query *string `form:"query"`
|
Query *string `form:"query"`
|
||||||
BookType *string `form:"book_type"`
|
Source *search.Source `form:"source"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type requestDocumentUpload struct {
|
type requestDocumentUpload struct {
|
||||||
@ -64,10 +64,10 @@ type requestSettingsEdit struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type requestDocumentAdd struct {
|
type requestDocumentAdd struct {
|
||||||
ID *string `form:"id"`
|
ID string `form:"id"`
|
||||||
Title *string `form:"title"`
|
Title *string `form:"title"`
|
||||||
Author *string `form:"author"`
|
Author *string `form:"author"`
|
||||||
BookType *string `form:"book_type"`
|
Source search.Source `form:"source"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) webManifest(c *gin.Context) {
|
func (api *API) webManifest(c *gin.Context) {
|
||||||
@ -240,25 +240,18 @@ func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any
|
|||||||
c.BindQuery(&sParams)
|
c.BindQuery(&sParams)
|
||||||
|
|
||||||
// Only Handle Query
|
// Only Handle Query
|
||||||
if sParams.BookType != nil && !slices.Contains([]string{"NON_FICTION", "FICTION"}, *sParams.BookType) {
|
if sParams.Query != nil && sParams.Source != nil {
|
||||||
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
|
// Search
|
||||||
searchResults, err := search.SearchBook(*sParams.Query, bType)
|
searchResults, err := search.SearchBook(*sParams.Query, *sParams.Source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("Search Error: %v", err))
|
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("Search Error: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
templateVars["Data"] = searchResults
|
templateVars["Data"] = searchResults
|
||||||
templateVars["BookType"] = *sParams.BookType
|
templateVars["Source"] = *sParams.Source
|
||||||
|
} else if sParams.Query != nil || sParams.Source != nil {
|
||||||
|
templateVars["SearchErrorMessage"] = "Invalid Query"
|
||||||
}
|
}
|
||||||
} else if routeName == "login" {
|
} else if routeName == "login" {
|
||||||
templateVars["RegistrationEnabled"] = api.Config.RegistrationEnabled
|
templateVars["RegistrationEnabled"] = api.Config.RegistrationEnabled
|
||||||
@ -762,23 +755,8 @@ func (api *API) saveNewDocument(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate Form Exists
|
|
||||||
if rDocAdd.ID == nil ||
|
|
||||||
rDocAdd.BookType == nil ||
|
|
||||||
rDocAdd.Title == nil ||
|
|
||||||
rDocAdd.Author == nil {
|
|
||||||
log.Error("[saveNewDocument] Missing Form Values")
|
|
||||||
errorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var bType search.BookType = search.BOOK_FICTION
|
|
||||||
if *rDocAdd.BookType == "NON_FICTION" {
|
|
||||||
bType = search.BOOK_NON_FICTION
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save Book
|
// Save Book
|
||||||
tempFilePath, err := search.SaveBook(*rDocAdd.ID, bType)
|
tempFilePath, err := search.SaveBook(rDocAdd.ID, rDocAdd.Source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("[saveNewDocument] Temp File Error: ", err)
|
log.Warn("[saveNewDocument] Temp File Error: ", err)
|
||||||
errorPage(c, http.StatusInternalServerError, "Unable to save file.")
|
errorPage(c, http.StatusInternalServerError, "Unable to save file.")
|
||||||
|
@ -26,6 +26,40 @@ var mimeMapping map[string]string = map[string]string{
|
|||||||
"lit": "application/x-ms-reader",
|
"lit": "application/x-ms-reader",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *API) opdsEntry(c *gin.Context) {
|
||||||
|
// Build & Return XML
|
||||||
|
mainFeed := &opds.Feed{
|
||||||
|
Title: "AnthoLume OPDS Server",
|
||||||
|
Updated: time.Now().UTC(),
|
||||||
|
Links: []opds.Link{
|
||||||
|
{
|
||||||
|
Title: "Search AnthoLume",
|
||||||
|
Rel: "search",
|
||||||
|
TypeLink: "application/opensearchdescription+xml",
|
||||||
|
Href: "/api/opds/search.xml",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Entries: []opds.Entry{
|
||||||
|
{
|
||||||
|
Title: "AnthoLume - All Documents",
|
||||||
|
Content: &opds.Content{
|
||||||
|
Content: "AnthoLume - All Documents",
|
||||||
|
ContentType: "text",
|
||||||
|
},
|
||||||
|
Links: []opds.Link{
|
||||||
|
{
|
||||||
|
Href: "/api/opds/documents?limit=100",
|
||||||
|
TypeLink: "application/atom+xml;type=feed;profile=opds-catalog",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
c.XML(http.StatusOK, mainFeed)
|
||||||
|
}
|
||||||
|
|
||||||
func (api *API) opdsDocuments(c *gin.Context) {
|
func (api *API) opdsDocuments(c *gin.Context) {
|
||||||
var userID string
|
var userID string
|
||||||
if rUser, _ := c.Get("AuthorizedUser"); rUser != nil {
|
if rUser, _ := c.Get("AuthorizedUser"); rUser != nil {
|
||||||
@ -35,9 +69,17 @@ func (api *API) opdsDocuments(c *gin.Context) {
|
|||||||
// Potential URL Parameters
|
// Potential URL Parameters
|
||||||
qParams := bindQueryParams(c)
|
qParams := bindQueryParams(c)
|
||||||
|
|
||||||
|
// Possible Query
|
||||||
|
var query *string
|
||||||
|
if qParams.Search != nil && *qParams.Search != "" {
|
||||||
|
search := "%" + *qParams.Search + "%"
|
||||||
|
query = &search
|
||||||
|
}
|
||||||
|
|
||||||
// Get Documents
|
// Get Documents
|
||||||
documents, err := api.DB.Queries.GetDocumentsWithStats(api.DB.Ctx, database.GetDocumentsWithStatsParams{
|
documents, err := api.DB.Queries.GetDocumentsWithStats(api.DB.Ctx, database.GetDocumentsWithStatsParams{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
|
Query: query,
|
||||||
Offset: (*qParams.Page - 1) * *qParams.Limit,
|
Offset: (*qParams.Page - 1) * *qParams.Limit,
|
||||||
Limit: *qParams.Limit,
|
Limit: *qParams.Limit,
|
||||||
})
|
})
|
||||||
@ -71,7 +113,7 @@ func (api *API) opdsDocuments(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
item := opds.Entry{
|
item := opds.Entry{
|
||||||
Title: fmt.Sprintf("[%3d%%] %s", int(doc.Percentage), title),
|
Title: title,
|
||||||
Author: []opds.Author{
|
Author: []opds.Author{
|
||||||
{
|
{
|
||||||
Name: author,
|
Name: author,
|
||||||
@ -84,12 +126,12 @@ func (api *API) opdsDocuments(c *gin.Context) {
|
|||||||
Links: []opds.Link{
|
Links: []opds.Link{
|
||||||
{
|
{
|
||||||
Rel: "http://opds-spec.org/acquisition",
|
Rel: "http://opds-spec.org/acquisition",
|
||||||
Href: fmt.Sprintf("./documents/%s/file", doc.ID),
|
Href: fmt.Sprintf("/api/opds/documents/%s/file", doc.ID),
|
||||||
TypeLink: mimeMapping[fileType],
|
TypeLink: mimeMapping[fileType],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Rel: "http://opds-spec.org/image",
|
Rel: "http://opds-spec.org/image",
|
||||||
Href: fmt.Sprintf("./documents/%s/cover", doc.ID),
|
Href: fmt.Sprintf("/api/opds/documents/%s/cover", doc.ID),
|
||||||
TypeLink: "image/jpeg",
|
TypeLink: "image/jpeg",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -99,19 +141,15 @@ func (api *API) opdsDocuments(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
feedTitle := "All Documents"
|
||||||
|
if query != nil {
|
||||||
|
feedTitle = "Search Results"
|
||||||
|
}
|
||||||
|
|
||||||
// Build & Return XML
|
// Build & Return XML
|
||||||
searchFeed := &opds.Feed{
|
searchFeed := &opds.Feed{
|
||||||
Title: "All Documents",
|
Title: feedTitle,
|
||||||
Updated: time.Now().UTC(),
|
Updated: time.Now().UTC(),
|
||||||
// TODO
|
|
||||||
// Links: []opds.Link{
|
|
||||||
// {
|
|
||||||
// Title: "Search AnthoLume",
|
|
||||||
// Rel: "search",
|
|
||||||
// TypeLink: "application/opensearchdescription+xml",
|
|
||||||
// Href: "search.xml",
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
Entries: allEntries,
|
Entries: allEntries,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,7 +160,7 @@ func (api *API) opdsSearchDescription(c *gin.Context) {
|
|||||||
rawXML := `<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
|
rawXML := `<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
|
||||||
<ShortName>Search AnthoLume</ShortName>
|
<ShortName>Search AnthoLume</ShortName>
|
||||||
<Description>Search AnthoLume</Description>
|
<Description>Search AnthoLume</Description>
|
||||||
<Url type="application/atom+xml;profile=opds-catalog;kind=acquisition" template="./search?query={searchTerms}"/>
|
<Url type="application/atom+xml;profile=opds-catalog;kind=acquisition" template="/api/opds/documents?limit=100&search={searchTerms}"/>
|
||||||
</OpenSearchDescription>`
|
</OpenSearchDescription>`
|
||||||
c.Data(http.StatusOK, "application/xml", []byte(rawXML))
|
c.Data(http.StatusOK, "application/xml", []byte(rawXML))
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ package config
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@ -22,6 +24,7 @@ type Config struct {
|
|||||||
RegistrationEnabled bool
|
RegistrationEnabled bool
|
||||||
SearchEnabled bool
|
SearchEnabled bool
|
||||||
DemoMode bool
|
DemoMode bool
|
||||||
|
LogLevel string
|
||||||
|
|
||||||
// Cookie Settings
|
// Cookie Settings
|
||||||
CookieSessionKey string
|
CookieSessionKey string
|
||||||
@ -30,7 +33,7 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Load() *Config {
|
func Load() *Config {
|
||||||
return &Config{
|
c := &Config{
|
||||||
Version: "0.0.1",
|
Version: "0.0.1",
|
||||||
DBType: trimLowerString(getEnv("DATABASE_TYPE", "SQLite")),
|
DBType: trimLowerString(getEnv("DATABASE_TYPE", "SQLite")),
|
||||||
DBName: trimLowerString(getEnv("DATABASE_NAME", "antholume")),
|
DBName: trimLowerString(getEnv("DATABASE_NAME", "antholume")),
|
||||||
@ -41,9 +44,19 @@ func Load() *Config {
|
|||||||
DemoMode: trimLowerString(getEnv("DEMO_MODE", "false")) == "true",
|
DemoMode: trimLowerString(getEnv("DEMO_MODE", "false")) == "true",
|
||||||
SearchEnabled: trimLowerString(getEnv("SEARCH_ENABLED", "false")) == "true",
|
SearchEnabled: trimLowerString(getEnv("SEARCH_ENABLED", "false")) == "true",
|
||||||
CookieSessionKey: trimLowerString(getEnv("COOKIE_SESSION_KEY", "")),
|
CookieSessionKey: trimLowerString(getEnv("COOKIE_SESSION_KEY", "")),
|
||||||
|
LogLevel: trimLowerString(getEnv("LOG_LEVEL", "info")),
|
||||||
CookieSecure: trimLowerString(getEnv("COOKIE_SECURE", "true")) == "true",
|
CookieSecure: trimLowerString(getEnv("COOKIE_SECURE", "true")) == "true",
|
||||||
CookieHTTPOnly: trimLowerString(getEnv("COOKIE_HTTP_ONLY", "true")) == "true",
|
CookieHTTPOnly: trimLowerString(getEnv("COOKIE_HTTP_ONLY", "true")) == "true",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log Level
|
||||||
|
ll, err := log.ParseLevel(c.LogLevel)
|
||||||
|
if err != nil {
|
||||||
|
ll = log.InfoLevel
|
||||||
|
}
|
||||||
|
log.SetLevel(ll)
|
||||||
|
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEnv(key, fallback string) string {
|
func getEnv(key, fallback string) string {
|
||||||
|
229
search/search.go
229
search/search.go
@ -2,6 +2,7 @@ package search
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -16,8 +17,8 @@ import (
|
|||||||
type Cadence string
|
type Cadence string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TOP_YEAR Cadence = "y"
|
CADENCE_TOP_YEAR Cadence = "y"
|
||||||
TOP_MONTH Cadence = "m"
|
CADENCE_TOP_MONTH Cadence = "m"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BookType int
|
type BookType int
|
||||||
@ -27,6 +28,14 @@ const (
|
|||||||
BOOK_NON_FICTION
|
BOOK_NON_FICTION
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Source string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SOURCE_ANNAS_ARCHIVE Source = "Annas Archive"
|
||||||
|
SOURCE_LIBGEN_FICTION Source = "LibGen Fiction"
|
||||||
|
SOURCE_LIBGEN_NON_FICTION Source = "LibGen Non-fiction"
|
||||||
|
)
|
||||||
|
|
||||||
type SearchItem struct {
|
type SearchItem struct {
|
||||||
ID string
|
ID string
|
||||||
Title string
|
Title string
|
||||||
@ -38,26 +47,89 @@ type SearchItem struct {
|
|||||||
UploadDate string
|
UploadDate string
|
||||||
}
|
}
|
||||||
|
|
||||||
func SearchBook(query string, bookType BookType) ([]SearchItem, error) {
|
type sourceDef struct {
|
||||||
if bookType == BOOK_FICTION {
|
searchURL string
|
||||||
// Search Fiction
|
downloadURL string
|
||||||
url := "https://libgen.is/fiction/?q=" + url.QueryEscape(query) + "&language=English&format=epub"
|
parseSearchFunc func(io.ReadCloser) ([]SearchItem, error)
|
||||||
body, err := getPage(url)
|
parseDownloadFunc func(io.ReadCloser) (string, error)
|
||||||
if err != nil {
|
}
|
||||||
return nil, err
|
|
||||||
}
|
var sourceDefs = map[Source]sourceDef{
|
||||||
return parseLibGenFiction(body)
|
SOURCE_ANNAS_ARCHIVE: {
|
||||||
} else if bookType == BOOK_NON_FICTION {
|
searchURL: "https://annas-archive.org/search?index=&q=%s&ext=epub&sort=&lang=en",
|
||||||
// Search NonFiction
|
downloadURL: "http://libgen.li/ads.php?md5=%s",
|
||||||
url := "https://libgen.is/search.php?req=" + url.QueryEscape(query)
|
parseSearchFunc: parseAnnasArchive,
|
||||||
body, err := getPage(url)
|
parseDownloadFunc: parseAnnasArchiveDownloadURL,
|
||||||
if err != nil {
|
},
|
||||||
return nil, err
|
SOURCE_LIBGEN_FICTION: {
|
||||||
}
|
searchURL: "https://libgen.is/fiction/?q=%s&language=English&format=epub",
|
||||||
return parseLibGenNonFiction(body)
|
downloadURL: "http://library.lol/fiction/%s",
|
||||||
} else {
|
parseSearchFunc: parseLibGenFiction,
|
||||||
return nil, errors.New("Invalid Book Type")
|
parseDownloadFunc: parseLibGenDownloadURL,
|
||||||
|
},
|
||||||
|
SOURCE_LIBGEN_NON_FICTION: {
|
||||||
|
searchURL: "https://libgen.is/search.php?req=%s",
|
||||||
|
downloadURL: "http://library.lol/main/%s",
|
||||||
|
parseSearchFunc: parseLibGenNonFiction,
|
||||||
|
parseDownloadFunc: parseLibGenDownloadURL,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func SearchBook(query string, source Source) ([]SearchItem, error) {
|
||||||
|
def := sourceDefs[source]
|
||||||
|
log.Debug("[SearchBook] Source: ", def)
|
||||||
|
url := fmt.Sprintf(def.searchURL, url.QueryEscape(query))
|
||||||
|
body, err := getPage(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return def.parseSearchFunc(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveBook(id string, source Source) (string, error) {
|
||||||
|
def := sourceDefs[source]
|
||||||
|
log.Debug("[SaveBook] Source: ", def)
|
||||||
|
url := fmt.Sprintf(def.downloadURL, id)
|
||||||
|
|
||||||
|
body, err := getPage(url)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
bookURL, err := def.parseDownloadFunc(body)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("[SaveBook] Parse Download URL Error: ", err)
|
||||||
|
return "", errors.New("Download Failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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: ", bookURL)
|
||||||
|
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 GoodReadsMostRead(c Cadence) ([]SearchItem, error) {
|
func GoodReadsMostRead(c Cadence) ([]SearchItem, error) {
|
||||||
@ -87,57 +159,9 @@ func GetBookURL(id string, bookType BookType) (string, error) {
|
|||||||
return 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, err := getPage(infoURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
bookURL, err := parseLibGenDownloadURL(body)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("[SaveBook] Parse Download URL Error: ", err)
|
|
||||||
return "", errors.New("Download Failure")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, error) {
|
func getPage(page string) (io.ReadCloser, error) {
|
||||||
|
log.Debug("[getPage] ", page)
|
||||||
|
|
||||||
// Set 10s Timeout
|
// Set 10s Timeout
|
||||||
client := http.Client{
|
client := http.Client{
|
||||||
Timeout: 10 * time.Second,
|
Timeout: 10 * time.Second,
|
||||||
@ -292,3 +316,66 @@ func parseGoodReads(body io.ReadCloser) ([]SearchItem, error) {
|
|||||||
// Return Results
|
// Return Results
|
||||||
return allEntries, nil
|
return allEntries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseAnnasArchiveDownloadURL(body io.ReadCloser) (string, error) {
|
||||||
|
// Parse
|
||||||
|
defer body.Close()
|
||||||
|
doc, _ := goquery.NewDocumentFromReader(body)
|
||||||
|
|
||||||
|
// Return Download URL
|
||||||
|
downloadURL, exists := doc.Find("body > table > tbody > tr > td > a").Attr("href")
|
||||||
|
if exists == false {
|
||||||
|
return "", errors.New("Download URL not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return "http://libgen.li/" + downloadURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAnnasArchive(body io.ReadCloser) ([]SearchItem, error) {
|
||||||
|
// Parse
|
||||||
|
defer body.Close()
|
||||||
|
doc, err := goquery.NewDocumentFromReader(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize Results
|
||||||
|
var allEntries []SearchItem
|
||||||
|
doc.Find("form > div.w-full > div.w-full > div > div.justify-center").Each(func(ix int, rawBook *goquery.Selection) {
|
||||||
|
// Parse Details
|
||||||
|
details := rawBook.Find("div:nth-child(2) > div:nth-child(1)").Text()
|
||||||
|
detailsSplit := strings.Split(details, ", ")
|
||||||
|
|
||||||
|
// Invalid Details
|
||||||
|
if len(detailsSplit) < 3 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
language := detailsSplit[0]
|
||||||
|
fileType := detailsSplit[1]
|
||||||
|
fileSize := detailsSplit[2]
|
||||||
|
|
||||||
|
// Get Title & Author
|
||||||
|
title := rawBook.Find("h3").Text()
|
||||||
|
author := rawBook.Find("div:nth-child(2) > div:nth-child(4)").Text()
|
||||||
|
|
||||||
|
// Parse MD5
|
||||||
|
itemHref, _ := rawBook.Find("a").Attr("href")
|
||||||
|
hrefArray := strings.Split(itemHref, "/")
|
||||||
|
id := hrefArray[len(hrefArray)-1]
|
||||||
|
|
||||||
|
item := SearchItem{
|
||||||
|
ID: id,
|
||||||
|
Title: title,
|
||||||
|
Author: author,
|
||||||
|
Language: language,
|
||||||
|
FileType: fileType,
|
||||||
|
FileSize: fileSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
allEntries = append(allEntries, item)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Return Results
|
||||||
|
return allEntries, nil
|
||||||
|
}
|
||||||
|
@ -36,7 +36,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex relative min-w-[12em]">
|
<div class="flex relative min-w-[12em]">
|
||||||
<span
|
<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"
|
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"
|
||||||
@ -61,11 +60,12 @@
|
|||||||
</span>
|
</span>
|
||||||
<select
|
<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"
|
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"
|
id="source"
|
||||||
name="book_type"
|
name="source"
|
||||||
>
|
>
|
||||||
<option value="FICTION">Fiction</option>
|
<option value="Annas Archive">Annas Archive</option>
|
||||||
<option value="NON_FICTION">Non-Fiction</option>
|
<option value="LibGen Fiction">LibGen Fiction</option>
|
||||||
|
<option value="LibGen Non-fiction">LibGen Non-fiction</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -136,9 +136,9 @@
|
|||||||
<input
|
<input
|
||||||
class="hidden"
|
class="hidden"
|
||||||
type="text"
|
type="text"
|
||||||
id="book_type"
|
id="source"
|
||||||
name="book_type"
|
name="source"
|
||||||
value="{{ $.BookType }}"
|
value="{{ $.Source }}"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
class="hidden"
|
class="hidden"
|
||||||
|
Loading…
Reference in New Issue
Block a user