Skip to content
This repository was archived by the owner on Jun 23, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .air.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"

[build]
args_bin = []
bin = "./tmp/codex"
cmd = "go build -o ./tmp/codex ./api/cmd/codex"
delay = 1000
exclude_dir = [
".git",
"tmp",
"vendor",
"testdata",
"node_modules",
".vscode",
".idea",
]
exclude_file = []
exclude_regex = ["_test\\.go$", "\\.git", "\\.DS_Store"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = ["api"]
include_ext = ["go", "mod", "sum", "env"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = true

[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"

[log]
main_only = false
silent = false
time = false

[misc]
clean_on_exit = false

[proxy]
app_port = 0
enabled = false
proxy_port = 0

[screen]
clear_on_rebuild = false
keep_scroll = true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.env
tmp
firestore.json
15 changes: 15 additions & 0 deletions api/internal/common/timestamp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package common

import "time"

func TimeStamp(date string) string {
if date != "" {
t, err := time.Parse("2006-01-02 15:04:05", date)
if err != nil {
return time.Now().Format("2006-01-02 15:04:05")
}
return t.Format("2006-01-02 15:04:05")
}

return time.Now().Format("2006-01-02 15:04:05")
}
15 changes: 14 additions & 1 deletion api/internal/domain/chapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,26 @@ type CursorResponse struct {
NextCursor string `json:"next_cursor"`
}

// Chapter struct used on backend
type Chapter struct {
ID string `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
Description string `json:"description"`
CreatedAt string `json:"creation_date"`
UploadedAt string `json:"upload_date"`
UpdatedAt string `json:"update_date"`
Content string `json:"content"`
Index int `json:"index"`
Deleted bool `json:"deleted"`
}

// Chapter struct used on frontend
type FrontendChapter struct {
ID string `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
Description string `json:"description"`
CreatedAt string `json:"creation_date"`
UpdatedAt string `json:"update_date"`
Content string `json:"content"`
}
13 changes: 12 additions & 1 deletion api/internal/domain/novel.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
package domain

// Novel struct used on backend
type Novel struct {
ID string `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
Description string `json:"description"`
CreatedAt string `json:"creation_date"`
UploadedAt string `json:"upload_date"`
UpdatedAt string `json:"update_date"`
Deleted bool `json:"deleted"`
}

// Novel struct used on frontend
type FrontendNovel struct {
ID string `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
Description string `json:"description"`
CreatedAt string `json:"creation_date"`
UpdatedAt string `json:"update_date"`
}
11 changes: 7 additions & 4 deletions api/internal/infrastructure/collections/chapters.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,11 @@ import (
"google.golang.org/grpc/status"
)

const Limit = 100

func (c *Client) CursorPagination(options domain.CursorOptions, ctx context.Context) (*domain.CursorResponse, error) {
coll := c.Client.Collection("novels").Doc(options.NovelID).Collection("chapters")
query := coll.OrderBy(firestore.DocumentID, options.SortBy)

limit := min(max(options.Limit, 1), Limit)
limit := min(max(options.Limit, 1), 100)

if options.Cursor == "" {
snaps, err := query.Limit(limit + 1).Documents(ctx).GetAll()
Expand Down Expand Up @@ -97,7 +95,10 @@ func (c *Client) BatchUploadChapters(novelId string, chapters []domain.Chapter,
for i := 0; i < len(chapters); i += chunkSize {
subset := chapters[i:min(i+chunkSize, len(chapters))]

bw := c.Client.BulkWriter(ctx)
batchCtx, cancel := context.WithTimeout(ctx, 300*time.Second)
defer cancel()

bw := c.Client.BulkWriter(batchCtx)
jobs := make([]*firestore.BulkWriterJob, 0, len(subset))

for _, chap := range subset {
Expand All @@ -124,6 +125,8 @@ func (c *Client) BatchUploadChapters(novelId string, chapters []domain.Chapter,
}
}
}

time.Sleep(100 * time.Millisecond)
}

return nil
Expand Down
36 changes: 36 additions & 0 deletions api/internal/interfaces/rest/handlers/novels.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,42 @@ import (
"github.com/gin-gonic/gin"
)

func EPUBNovel(c *gin.Context) {
ctx := c.Request.Context()
defer ctx.Done()

defer func() {
if c.Request.MultipartForm != nil {
c.Request.MultipartForm.RemoveAll()
}
}()

epubFile, err := c.FormFile("file")
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": "Failed to get EPUB file: " + err.Error(),
})
return
}

err = firestore_services.CreateNovelFromEPUB(epubFile, ctx)
if e, ok := err.(*cmn.Error); ok {
c.AbortWithStatusJSON(e.StatusCode(), gin.H{
"error": "Failed to upload EPUB file: " + e.Error(),
})
return
} else if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": "Failed to upload EPUB file: " + err.Error(),
})
return
}

c.JSON(http.StatusOK, gin.H{
"message": "EPUB file uploaded successfully",
})
}

func FindNovel(c *gin.Context) {
ctx := c.Request.Context()
defer ctx.Done()
Expand Down
3 changes: 2 additions & 1 deletion api/internal/interfaces/rest/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func RegisteredRoutes(r *gin.Engine) {
}))

r.Use(firestore_middleware.RateLimiter())
r.MaxMultipartMemory = 32 << 20 // 32 MB

client := r.Group("/")
{
Expand All @@ -53,7 +54,7 @@ func RegisteredRoutes(r *gin.Engine) {
{
manage.POST("/novel", firestore_middleware.ValidateToken(), firestore_handlers.CreateNovel)
manage.POST("/:novel/chapter", firestore_middleware.ValidateToken(), firestore_handlers.CreateChapter)
manage.POST("/:novel", firestore_middleware.ValidateToken(), firestore_handlers.BatchUploadChapters)
manage.POST("/epub", firestore_middleware.ValidateToken(), firestore_handlers.EPUBNovel)
manage.PUT("/:novel", firestore_middleware.ValidateToken(), firestore_handlers.UpdateNovel)
manage.PUT("/:novel/:chapter", firestore_middleware.ValidateToken(), firestore_handlers.UpdateChapter)
manage.DELETE("/:novel", firestore_middleware.ValidateToken(), firestore_handlers.DeleteNovel)
Expand Down
4 changes: 2 additions & 2 deletions api/internal/usecases/collections/chapters.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func BatchUploadChapters(novelId string, chapters []domain.Chapter, ctx context.
chapter.ID = id
chapter.CreatedAt = time.Now().Format("2006-01-02 15:04:05")
chapter.UpdatedAt = time.Now().Format("2006-01-02 15:04:05")
chapter.UploadedAt = time.Now().Format("2006-01-02 15:04:05")
chapter.Deleted = false

final = append(final, chapter)
}
Expand Down Expand Up @@ -89,7 +89,7 @@ func CreateChapter(novelId string, chapter domain.Chapter, ctx context.Context)
chapter.ID = id
chapter.CreatedAt = time.Now().Format("2006-01-02 15:04:05")
chapter.UpdatedAt = time.Now().Format("2006-01-02 15:04:05")
chapter.UploadedAt = time.Now().Format("2006-01-02 15:04:05")
chapter.Deleted = false

err = c.CreateChapter(novelId, chapter, ctx)
if err != nil {
Expand Down
117 changes: 116 additions & 1 deletion api/internal/usecases/collections/novels.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,125 @@ import (
firestore_collections "Codex-Backend/api/internal/infrastructure/collections"
"context"
"errors"
"io"
"mime/multipart"
"net/http"
"strings"
"time"

htmltomarkdown "github.com/JohannesKaufmann/html-to-markdown/v2"
"github.com/PuerkitoBio/goquery"
"github.com/timsims/pamphlet"
)

// Remove HTML tags and convert the body to Markdown
func cleanHtml(input string) (string, error) {
doc, err := goquery.NewDocumentFromReader(strings.NewReader(input))
if err != nil {
return "", err
}

doc.Find("head, script, style, nav").Remove()

html, err := doc.Find("body").Html()
if err != nil {
return "", err
}

return htmltomarkdown.ConvertString(html)
}

func CreateNovelFromEPUB(epubFile *multipart.FileHeader, ctx context.Context) error {
multipartFIle, err := epubFile.Open()
if err != nil {
return err
}
defer multipartFIle.Close()

data, err := io.ReadAll(multipartFIle)
if err != nil {
return err
}

parser, err := pamphlet.OpenBytes(data)
if err != nil {
return err
}
defer parser.Close()

book := parser.GetBook()

// Create Novel

id, err := cmn.GenerateID("novel")
if err != nil {
return err
}

description, err := cleanHtml(book.Description)
if err != nil {
return err
}

novel := &domain.Novel{
ID: id,
Title: book.Title,
Author: book.Author,
Description: description,
CreatedAt: cmn.TimeStamp(book.Date),
UpdatedAt: cmn.TimeStamp(""),
Deleted: false,
}

err, id = CreateNovel(*novel, ctx)
if err != nil {
return err
}

// Create chapters

rawChapters := book.Chapters

c_id, err := cmn.GenerateID("chapter")
if err != nil {
return err
}

chapters := make([]domain.Chapter, len(rawChapters))
for i, chapter := range rawChapters {
rawContent, err := chapter.GetContent()
if err != nil {
return err
}

content, err := cleanHtml(rawContent)
if err != nil {
return err
}

chapter := &domain.Chapter{
ID: c_id,
Title: chapter.Title,
Author: book.Author,
Description: "",
CreatedAt: cmn.TimeStamp(""),
UpdatedAt: cmn.TimeStamp(""),
Content: content,
Index: i,
Deleted: false,
}

chapters[i] = *chapter
}

err = BatchUploadChapters(id, chapters, ctx)
if err != nil {
return err
}

return nil
}

func CreateNovel(novel domain.Novel, ctx context.Context) (error, string) {
client, err := firestore_client.FirestoreClient()
if err != nil {
Expand All @@ -28,7 +143,7 @@ func CreateNovel(novel domain.Novel, ctx context.Context) (error, string) {
novel.ID = id
novel.CreatedAt = time.Now().Format("2006-01-02 15:04:05")
novel.UpdatedAt = time.Now().Format("2006-01-02 15:04:05")
novel.UploadedAt = time.Now().Format("2006-01-02 15:04:05")
novel.Deleted = false

err = c.CreateNovel(novel, ctx)
if err != nil {
Expand Down
Loading