Files
bookhoard/internal/services/conversion_service.go
T
john-okeefe e389df92c3 refactor: remove unnecessary type conversions and handle ignored errors across codebase
Remove redundant type conversions that Go 1.26 makes unnecessary or that
were already no-ops:

- uuid.UUID(x.Bytes) → x.Bytes (uuid.UUID is [16]byte, same as pgtype UUID Bytes)
- pgtype.UUID{Bytes: [16]byte(u), Valid: true} → pgtype.UUID{Bytes: u, Valid: true}
- (*time.Time)(&x.Time) → &x.Time
- json.RawMessage(x) → x where x is already []byte
- []byte(stringVal) → stringVal where []byte is expected
- int()/int64()/byte() casts on values already of the target type
- Decompressor(fn) → fn (type is identical)

Handle previously ignored error returns:

- collections.go: check json.Unmarshal error in GetCollection
- conversion_service.go: check fileSize.Scan() error
- app_test.go: check app.Shutdown() error in benchmark
2026-04-21 21:15:59 -04:00

140 lines
3.7 KiB
Go

package services
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"time"
"bookhoard/internal/database"
"github.com/jackc/pgx/v5/pgtype"
)
type ConvertedKEPUB struct {
Path string
SHA256 string
Cached bool
}
type ConversionService struct {
db *database.Queries
cacheDir string
conversionTool string
conversionCacheTTL time.Duration
}
func NewConversionService(db *database.Queries, cacheDir string) *ConversionService {
return &ConversionService{
db: db,
cacheDir: cacheDir,
conversionTool: "/usr/bin/kepubify",
conversionCacheTTL: 24 * time.Hour,
}
}
func (s *ConversionService) ConvertEPUBToKEPUB(ctx context.Context, mediaItemID pgtype.UUID, epubPath string) (*ConvertedKEPUB, error) {
existing, err := s.db.GetMediaItemFormatByType(ctx, database.GetMediaItemFormatByTypeParams{
MediaItemID: mediaItemID,
FormatType: "kepub",
})
if err == nil && existing.FilePath.Valid {
if time.Since(existing.CreatedAt.Time) < s.conversionCacheTTL {
return &ConvertedKEPUB{
Path: existing.FilePath.String,
SHA256: existing.FileSha256.String,
Cached: true,
}, nil
}
}
kepubPath := filepath.Join(s.cacheDir, fmt.Sprintf("%s.kepub.epub", mediaItemID.Bytes[:]))
if err := os.MkdirAll(s.cacheDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create cache directory: %w", err)
}
if err := s.convertEPUB(epubPath, kepubPath); err != nil {
return nil, fmt.Errorf("conversion failed: %w", err)
}
kepubSHA256, err := s.calculateSHA256(kepubPath)
if err != nil {
return nil, fmt.Errorf("hash calculation failed: %w", err)
}
epubFormat, err := s.db.GetMediaItemFormatByType(ctx, database.GetMediaItemFormatByTypeParams{
MediaItemID: mediaItemID,
FormatType: "epub",
})
if err != nil {
return nil, fmt.Errorf("EPUB format not found: %w", err)
}
fileinfo, err := os.Stat(kepubPath)
if err != nil {
return nil, fmt.Errorf("failed to stat converted file: %w", err)
}
var fileSize pgtype.Int8
err = fileSize.Scan(fileinfo.Size())
if err != nil {
return nil, err
}
_, err = s.db.CreateMediaItemFormat(ctx, database.CreateMediaItemFormatParams{
MediaItemID: mediaItemID,
FormatType: "kepub",
FilePath: pgtype.Text{String: kepubPath, Valid: true},
FileSha256: pgtype.Text{String: kepubSHA256, Valid: true},
FileSizeBytes: fileSize,
MimeType: pgtype.Text{String: "application/vnd.kobo+xml+zip", Valid: true},
ConvertedFromFormatID: pgtype.UUID{Bytes: epubFormat.ID.Bytes, Valid: true},
})
if err != nil {
return nil, fmt.Errorf("failed to store converted format: %w", err)
}
return &ConvertedKEPUB{
Path: kepubPath,
SHA256: kepubSHA256,
Cached: false,
}, nil
}
func (s *ConversionService) convertEPUB(epubPath, kepubPath string) error {
if _, err := os.Stat(s.conversionTool); err == nil {
cmd := exec.Command(s.conversionTool, "-i", epubPath, "-o", kepubPath)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("kepubify failed: %w, output: %s", err, string(output))
}
return nil
}
cmd := exec.Command("ebook-convert", epubPath, kepubPath, "--output-format", "kepub")
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("ebook-convert failed: %w, output: %s", err, string(output))
}
return nil
}
func (s *ConversionService) calculateSHA256(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
hasher := sha256.New()
if _, err := io.Copy(hasher, file); err != nil {
return "", err
}
return hex.EncodeToString(hasher.Sum(nil)), nil
}