Compare commits
2 Commits
master
...
5f8a9b7b14
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f8a9b7b14 | |||
| 13df4ae706 |
@@ -3,7 +3,7 @@ FROM alpine AS alpine
|
|||||||
RUN apk update && apk add --no-cache ca-certificates tzdata
|
RUN apk update && apk add --no-cache ca-certificates tzdata
|
||||||
|
|
||||||
# Build Image
|
# Build Image
|
||||||
FROM golang:1.24 AS build
|
FROM golang:1.21 AS build
|
||||||
|
|
||||||
# Create Package Directory
|
# Create Package Directory
|
||||||
RUN mkdir -p /opt/antholume
|
RUN mkdir -p /opt/antholume
|
||||||
|
|||||||
8
flake.lock
generated
8
flake.lock
generated
@@ -20,16 +20,16 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1764522689,
|
"lastModified": 1754292888,
|
||||||
"narHash": "sha256-SqUuBFjhl/kpDiVaKLQBoD8TLD+/cTUzzgVFoaHrkqY=",
|
"narHash": "sha256-1ziydHSiDuSnaiPzCQh1mRFBsM2d2yRX9I+5OPGEmIE=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "8bb5646e0bed5dbd3ab08c7a7cc15b75ab4e1d0f",
|
"rev": "ce01daebf8489ba97bd1609d185ea276efdeb121",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"ref": "nixos-25.11",
|
"ref": "nixos-25.05",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|||||||
13
flake.nix
13
flake.nix
@@ -2,18 +2,12 @@
|
|||||||
description = "Development Environment";
|
description = "Development Environment";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
{ self
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
, nixpkgs
|
|
||||||
, flake-utils
|
|
||||||
,
|
|
||||||
}:
|
|
||||||
flake-utils.lib.eachDefaultSystem (
|
|
||||||
system:
|
|
||||||
let
|
let
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
in
|
in
|
||||||
@@ -21,7 +15,6 @@
|
|||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
go
|
go
|
||||||
gopls
|
|
||||||
golangci-lint
|
golangci-lint
|
||||||
nodejs
|
nodejs
|
||||||
tailwindcss
|
tailwindcss
|
||||||
|
|||||||
@@ -53,12 +53,10 @@ func countEPUBWords(filepath string) (int64, error) {
|
|||||||
rf := rc.Rootfiles[0]
|
rf := rc.Rootfiles[0]
|
||||||
|
|
||||||
var completeCount int64
|
var completeCount int64
|
||||||
for _, item := range rf.Itemrefs {
|
for _, item := range rf.Spine.Itemrefs {
|
||||||
f, _ := item.Open()
|
f, _ := item.Open()
|
||||||
doc, _ := goquery.NewDocumentFromReader(f)
|
doc, _ := goquery.NewDocumentFromReader(f)
|
||||||
doc.Find("script, style, noscript, iframe").Remove()
|
completeCount = completeCount + int64(len(strings.Fields(doc.Text())))
|
||||||
words := len(strings.Fields(doc.Text()))
|
|
||||||
completeCount = completeCount + int64(words)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return completeCount, nil
|
return completeCount, nil
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ func GetWordCount(filepath string) (*int64, error) {
|
|||||||
}
|
}
|
||||||
return &totalWords, nil
|
return &totalWords, nil
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("invalid extension: %s", fileExtension)
|
return nil, fmt.Errorf("Invalid extension: %s", fileExtension)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestGetWordCount(t *testing.T) {
|
func TestGetWordCount(t *testing.T) {
|
||||||
var desiredCount int64 = 30070
|
var desiredCount int64 = 30080
|
||||||
actualCount, err := countEPUBWords("../_test_files/alice.epub")
|
actualCount, err := countEPUBWords("../_test_files/alice.epub")
|
||||||
|
|
||||||
assert.Nil(t, err, "should have no error")
|
assert.Nil(t, err, "should have no error")
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package formatters
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFormatDuration(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
dur time.Duration
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{0, "N/A"},
|
|
||||||
{22*24*time.Hour + 7*time.Hour + 39*time.Minute + 31*time.Second, "22d 7h 39m 31s"},
|
|
||||||
{5*time.Minute + 15*time.Second, "5m 15s"},
|
|
||||||
}
|
|
||||||
for _, tc := range tests {
|
|
||||||
if got := FormatDuration(tc.dur); got != tc.want {
|
|
||||||
t.Errorf("FormatDuration(%v) = %s, want %s", tc.dur, got, tc.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package formatters
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFormatNumber(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
input int64
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{0, "0"},
|
|
||||||
{19823, "19.8k"},
|
|
||||||
{1500000, "1.50M"},
|
|
||||||
{-12345, "-12.3k"},
|
|
||||||
}
|
|
||||||
for _, tc := range tests {
|
|
||||||
if got := FormatNumber(tc.input); got != tc.want {
|
|
||||||
t.Errorf("FormatNumber(%d) = %s, want %s", tc.input, got, tc.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
package ptr
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestOf(t *testing.T) {
|
|
||||||
// Test with different types
|
|
||||||
intVal := 42
|
|
||||||
intPtr := Of(intVal)
|
|
||||||
if *intPtr != intVal {
|
|
||||||
t.Errorf("Expected %d, got %d", intVal, *intPtr)
|
|
||||||
}
|
|
||||||
|
|
||||||
stringVal := "hello"
|
|
||||||
stringPtr := Of(stringVal)
|
|
||||||
if *stringPtr != stringVal {
|
|
||||||
t.Errorf("Expected %s, got %s", stringVal, *stringPtr)
|
|
||||||
}
|
|
||||||
|
|
||||||
floatVal := 3.14
|
|
||||||
floatPtr := Of(floatVal)
|
|
||||||
if *floatPtr != floatVal {
|
|
||||||
t.Errorf("Expected %f, got %f", floatVal, *floatPtr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeref(t *testing.T) {
|
|
||||||
// Test with non-nil pointer
|
|
||||||
intVal := 42
|
|
||||||
intPtr := Of(intVal)
|
|
||||||
result := Deref(intPtr)
|
|
||||||
if result != intVal {
|
|
||||||
t.Errorf("Expected %d, got %d", intVal, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with nil pointer
|
|
||||||
var nilPtr *int
|
|
||||||
result = Deref(nilPtr)
|
|
||||||
if result != 0 {
|
|
||||||
t.Errorf("Expected 0, got %d", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with string
|
|
||||||
stringVal := "hello"
|
|
||||||
stringPtr := Of(stringVal)
|
|
||||||
resultStr := Deref(stringPtr)
|
|
||||||
if resultStr != stringVal {
|
|
||||||
t.Errorf("Expected %s, got %s", stringVal, resultStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with nil string pointer
|
|
||||||
var nilStrPtr *string
|
|
||||||
resultStr = Deref(nilStrPtr)
|
|
||||||
if resultStr != "" {
|
|
||||||
t.Errorf("Expected empty string, got %s", resultStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDerefZeroValue(t *testing.T) {
|
|
||||||
// Test that Deref returns zero value for nil pointers
|
|
||||||
var nilInt *int
|
|
||||||
result := Deref(nilInt)
|
|
||||||
if result != 0 {
|
|
||||||
t.Errorf("Expected zero int, got %d", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
var nilString *string
|
|
||||||
resultStr := Deref(nilString)
|
|
||||||
if resultStr != "" {
|
|
||||||
t.Errorf("Expected zero string, got %s", resultStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package sliceutils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFirst(t *testing.T) {
|
|
||||||
// Test with empty slice
|
|
||||||
var empty []int
|
|
||||||
result, ok := First(empty)
|
|
||||||
if ok != false {
|
|
||||||
t.Errorf("Expected ok=false for empty slice, got %v", ok)
|
|
||||||
}
|
|
||||||
if result != 0 {
|
|
||||||
t.Errorf("Expected zero value for empty slice, got %v", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with non-empty slice
|
|
||||||
testSlice := []int{1, 2, 3}
|
|
||||||
result, ok = First(testSlice)
|
|
||||||
if ok != true {
|
|
||||||
t.Errorf("Expected ok=true for non-empty slice, got %v", ok)
|
|
||||||
}
|
|
||||||
if result != 1 {
|
|
||||||
t.Errorf("Expected first element, got %v", result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMap(t *testing.T) {
|
|
||||||
// Test with empty slice
|
|
||||||
var empty []int
|
|
||||||
result := Map(empty, func(x int) int { return x * 2 })
|
|
||||||
if len(result) != 0 {
|
|
||||||
t.Errorf("Expected empty result for empty input, got %v", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with non-empty slice
|
|
||||||
testSlice := []int{1, 2, 3}
|
|
||||||
result = Map(testSlice, func(x int) int { return x * 2 })
|
|
||||||
expected := []int{2, 4, 6}
|
|
||||||
if len(result) != len(expected) {
|
|
||||||
t.Errorf("Expected length %d, got %d", len(expected), len(result))
|
|
||||||
}
|
|
||||||
for i, v := range result {
|
|
||||||
if v != expected[i] {
|
|
||||||
t.Errorf("Expected %d at index %d, got %d", expected[i], i, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestTernary(t *testing.T) {
|
|
||||||
// Test true condition
|
|
||||||
result := Ternary(true, 42, 13)
|
|
||||||
if result != 42 {
|
|
||||||
t.Errorf("Expected 42, got %d", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test false condition
|
|
||||||
result = Ternary(false, 42, 13)
|
|
||||||
if result != 13 {
|
|
||||||
t.Errorf("Expected 13, got %d", result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFirstNonZero(t *testing.T) {
|
|
||||||
// Test with int values
|
|
||||||
result := FirstNonZero(0, 0, 42, 13)
|
|
||||||
if result != 42 {
|
|
||||||
t.Errorf("Expected 42, got %d", result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with string values
|
|
||||||
resultStr := FirstNonZero("", "", "hello")
|
|
||||||
if resultStr != "hello" {
|
|
||||||
t.Errorf("Expected hello, got %s", resultStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test all zero values (strings)
|
|
||||||
zeroResultStr := FirstNonZero("")
|
|
||||||
if zeroResultStr != "" {
|
|
||||||
t.Errorf("Expected empty string, got %s", zeroResultStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with float values
|
|
||||||
floatResult := FirstNonZero(0.0, 0.0, 3.14)
|
|
||||||
if floatResult != 3.14 {
|
|
||||||
t.Errorf("Expected 3.14, got %f", floatResult)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,11 +4,14 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var commentRE = regexp.MustCompile(`(?s)<!--(.*?)-->`)
|
||||||
|
|
||||||
func searchAnnasArchive(query string) ([]SearchItem, error) {
|
func searchAnnasArchive(query string) ([]SearchItem, error) {
|
||||||
searchURL := "https://annas-archive.org/search?index=&q=%s&ext=epub&sort=&lang=en"
|
searchURL := "https://annas-archive.org/search?index=&q=%s&ext=epub&sort=&lang=en"
|
||||||
url := fmt.Sprintf(searchURL, url.QueryEscape(query))
|
url := fmt.Sprintf(searchURL, url.QueryEscape(query))
|
||||||
@@ -29,34 +32,62 @@ func parseAnnasArchive(body io.ReadCloser) ([]SearchItem, error) {
|
|||||||
|
|
||||||
// Normalize Results
|
// Normalize Results
|
||||||
var allEntries []SearchItem
|
var allEntries []SearchItem
|
||||||
doc.Find(".js-aarecord-list-outer > div > div").Each(func(ix int, rawBook *goquery.Selection) {
|
doc.Find("#aarecord-list > div.justify-center").Each(func(ix int, rawBook *goquery.Selection) {
|
||||||
|
rawBook = getAnnasArchiveBookSelection(rawBook)
|
||||||
|
|
||||||
// Parse Details
|
// Parse Details
|
||||||
details := rawBook.Find("div:nth-child(3)").Text()
|
details := rawBook.Find("div:nth-child(2) > div:nth-child(1)").Text()
|
||||||
detailsSplit := strings.Split(details, " · ")
|
detailsSplit := strings.Split(details, ", ")
|
||||||
|
|
||||||
// Invalid Details
|
// Invalid Details
|
||||||
if len(detailsSplit) < 3 {
|
if len(detailsSplit) < 4 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse MD5
|
// Parse MD5
|
||||||
titleAuthorDetails := rawBook.Find("div a")
|
itemHref, _ := rawBook.Find("a").Attr("href")
|
||||||
titleEl := titleAuthorDetails.Eq(0)
|
|
||||||
itemHref, _ := titleEl.Attr("href")
|
|
||||||
hrefArray := strings.Split(itemHref, "/")
|
hrefArray := strings.Split(itemHref, "/")
|
||||||
id := hrefArray[len(hrefArray)-1]
|
id := hrefArray[len(hrefArray)-1]
|
||||||
|
|
||||||
allEntries = append(allEntries, SearchItem{
|
allEntries = append(allEntries, SearchItem{
|
||||||
ID: id,
|
ID: id,
|
||||||
Title: titleEl.Text(),
|
Title: rawBook.Find("h3").First().Text(),
|
||||||
Author: titleAuthorDetails.Eq(1).Text(),
|
Author: rawBook.Find("div:nth-child(2) > div:nth-child(4)").First().Text(),
|
||||||
Language: detailsSplit[0],
|
Language: detailsSplit[0],
|
||||||
FileType: detailsSplit[1],
|
FileType: detailsSplit[1],
|
||||||
FileSize: detailsSplit[2],
|
FileSize: detailsSplit[3],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Return Results
|
// Return Results
|
||||||
return allEntries, nil
|
return allEntries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getAnnasArchiveBookSelection parses potentially commented out HTML. For some reason
|
||||||
|
// Annas Archive comments out blocks "below the fold". They aren't rendered until you
|
||||||
|
// scroll. This attempts to parse the commented out HTML.
|
||||||
|
func getAnnasArchiveBookSelection(rawBook *goquery.Selection) *goquery.Selection {
|
||||||
|
rawHTML, err := rawBook.Html()
|
||||||
|
if err != nil {
|
||||||
|
return rawBook
|
||||||
|
}
|
||||||
|
|
||||||
|
strippedHTML := strings.TrimSpace(rawHTML)
|
||||||
|
if !strings.HasPrefix(strippedHTML, "<!--") || !strings.HasSuffix(strippedHTML, "-->") {
|
||||||
|
return rawBook
|
||||||
|
}
|
||||||
|
|
||||||
|
allMatches := commentRE.FindAllStringSubmatch(strippedHTML, -1)
|
||||||
|
if len(allMatches) != 1 || len(allMatches[0]) != 2 {
|
||||||
|
return rawBook
|
||||||
|
}
|
||||||
|
|
||||||
|
captureGroup := allMatches[0][1]
|
||||||
|
docReader := strings.NewReader(captureGroup)
|
||||||
|
doc, err := goquery.NewDocumentFromReader(docReader)
|
||||||
|
if err != nil {
|
||||||
|
return rawBook
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc.Selection
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user