package storage import ( "bytes" "context" "errors" "fmt" "io" "regexp" "strings" "github.com/libnovel/backend/internal/bookstore" "github.com/minio/minio-go/v7" ) var ( chapterPattern = regexp.MustCompile(`(?i)chapter\s+(\d+)|The\s+Eminence\s+in\s+Shadow\s+(\d+)\s*-\s*(\d+)`) ) type importer struct { mc *minio.Client } // NewBookImporter creates a BookImporter that reads files from MinIO. func NewBookImporter(mc *minio.Client) bookstore.BookImporter { return &importer{mc: mc} } func (i *importer) Import(ctx context.Context, objectKey, fileType string) ([]bookstore.Chapter, error) { if fileType != "pdf" && fileType != "epub" { return nil, fmt.Errorf("unsupported file type: %s", fileType) } obj, err := i.mc.GetObject(ctx, "imports", objectKey, minio.GetObjectOptions{}) if err != nil { return nil, fmt.Errorf("get object from minio: %w", err) } defer obj.Close() data, err := io.ReadAll(obj) if err != nil { return nil, fmt.Errorf("read object: %w", err) } if fileType == "pdf" { return i.parsePDF(data) } return i.parseEPUB(data) } func (i *importer) parsePDF(data []byte) ([]bookstore.Chapter, error) { return nil, errors.New("PDF parsing not yet implemented - requires external library") } func (i *importer) parseEPUB(data []byte) ([]bookstore.Chapter, error) { return nil, errors.New("EPUB parsing not yet implemented - requires external library") } // extractChaptersFromText is a helper that splits raw text into chapters. // Used as a fallback when the PDF parser library returns raw text. func extractChaptersFromText(text string) []bookstore.Chapter { var chapters []bookstore.Chapter var currentChapter *bookstore.Chapter lines := strings.Split(text, "\n") chapterNum := 0 for _, line := range lines { line = strings.TrimSpace(line) if len(line) < 3 { continue } matches := chapterPattern.FindStringSubmatch(line) if matches != nil { if currentChapter != nil && currentChapter.Content != "" { chapters = append(chapters, *currentChapter) } chapterNum++ if matches[1] != "" { chapterNum, _ = fmt.Sscanf(matches[1], "%d", &chapterNum) } currentChapter = &bookstore.Chapter{ Number: chapterNum, Title: line, Content: "", } continue } if currentChapter != nil { if currentChapter.Content != "" { currentChapter.Content += " " } currentChapter.Content += line } } if currentChapter != nil && currentChapter.Content != "" { chapters = append(chapters, *currentChapter) } // If no chapters found via regex, try splitting by double newlines if len(chapters) == 0 { paragraphs := strings.Split(text, "\n\n") for i, para := range paragraphs { para = strings.TrimSpace(para) if len(para) > 50 { chapters = append(chapters, bookstore.Chapter{ Number: i + 1, Title: fmt.Sprintf("Chapter %d", i+1), Content: para, }) } } } return chapters } // IngestChapters stores extracted chapters for a book via BookWriter. // This is called by the runner after extracting chapters from PDF/EPUB. func (s *Store) IngestChapters(ctx context.Context, slug string, chapters []bookstore.Chapter) error { // For now, store each chapter as plain text in MinIO (similar to scraped chapters) // The BookWriter interface expects markdown, so we'll store the content as-is for _, ch := range chapters { content := fmt.Sprintf("# Chapter %d\n\n%s", ch.Number, ch.Content) if ch.Title != "" { content = fmt.Sprintf("# %s\n\n%s", ch.Title, ch.Content) } key := fmt.Sprintf("books/%s/chapters/%d.md", slug, ch.Number) if err := s.mc.PutObject(ctx, "books", key, strings.NewReader(content), int64(len(content)), minio.PutObjectOptions{ ContentType: "text/markdown", }); err != nil { return fmt.Errorf("put chapter %d: %w", ch.Number, err) } } // Also create a simple metadata entry in the books collection // (in a real implementation, we'd update the existing book or create a placeholder) return nil } // GetImportObjectKey returns the MinIO object key for an uploaded import file. func GetImportObjectKey(filename string) string { return fmt.Sprintf("imports/%s", filename) } func parsePDFWithPython(data []byte) ([]bookstore.Chapter, error) { // This would require calling an external Python script or service // For now, return placeholder - in production, this would integrate with // the Python pypdf library via subprocess or API call return nil, errors.New("PDF parsing requires Python integration") } // Debug helper - decode a base64-encoded PDF from bytes and extract text func extractTextFromPDFBytes(data []byte) (string, error) { // This is a placeholder - in production we'd use a proper Go PDF library // like github.com/ledongthuc/pdf or the Python approach var buf bytes.Buffer _, err := buf.Write(data) if err != nil { return "", err } return "", errors.New("PDF text extraction not implemented in Go") }