mirror of
https://github.com/Belphemur/CBZOptimizer.git
synced 2026-07-21 19:05:39 +02:00
- Use named returns for IsAlreadyConverted and WriteChapterToCBZ so errs.Capture defers propagate close errors to the caller. - Add context.Context to IsAlreadyConverted and ExtractChapter for cancellation/timeout support; thread it from Optimize. - Filter non-image files (__MACOSX, Thumbs.db, .DS_Store, etc.) and only extract supported image extensions in ExtractChapter. - Validate TempDir is non-empty in ConvertChapter before creating output dir. - Separate fatal conversion errors from PageIgnoredError in the WebP converter result aggregation so real failures aren't masked. - Fix error message "failed to load chapter" → "failed to extract chapter". - Use %w instead of %v in fmt.Errorf for proper error wrapping. - Fix LoadChapter doc comment to match its actual behavior. - Extract shared parseConvertedComment/parseConvertedCommentTime helpers to avoid duplicated zip-comment parsing logic. - Convert tests to use testify assertions (assert/require) per guidelines. Co-authored-by: Belphemur <197810+Belphemur@users.noreply.github.com>
161 lines
5.3 KiB
Go
161 lines
5.3 KiB
Go
package utils
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/belphemur/CBZOptimizer/v2/internal/cbz"
|
|
"github.com/belphemur/CBZOptimizer/v2/pkg/converter"
|
|
errors2 "github.com/belphemur/CBZOptimizer/v2/pkg/converter/errors"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
type OptimizeOptions struct {
|
|
ChapterConverter converter.Converter
|
|
Path string
|
|
Quality uint8
|
|
Override bool
|
|
Split bool
|
|
Timeout time.Duration
|
|
}
|
|
|
|
// Optimize optimizes a CBZ/CBR file using the specified converter.
|
|
// The new pipeline is disk-first:
|
|
// 1. Fast check if already converted (no extraction)
|
|
// 2. Extract archive to temp directory on disk
|
|
// 3. Convert pages file-to-file (no image data in memory)
|
|
// 4. Create output CBZ by streaming from disk
|
|
// 5. Cleanup temp files
|
|
func Optimize(options *OptimizeOptions) error {
|
|
log.Info().Str("file", options.Path).Msg("Processing file")
|
|
log.Debug().
|
|
Str("file", options.Path).
|
|
Uint8("quality", options.Quality).
|
|
Bool("override", options.Override).
|
|
Bool("split", options.Split).
|
|
Msg("Optimization parameters")
|
|
|
|
// Step 1: Fast conversion check before extracting (new requirement)
|
|
alreadyConverted, err := cbz.IsAlreadyConverted(context.Background(), options.Path)
|
|
if err != nil {
|
|
log.Debug().Str("file", options.Path).Err(err).Msg("Conversion check failed, proceeding with extraction")
|
|
}
|
|
if alreadyConverted {
|
|
log.Info().Str("file", options.Path).Msg("Chapter already converted")
|
|
return nil
|
|
}
|
|
|
|
// Step 2: Extract chapter to disk
|
|
log.Debug().Str("file", options.Path).Msg("Extracting chapter")
|
|
|
|
// Create context for extraction (use timeout if configured)
|
|
var extractCtx context.Context
|
|
if options.Timeout > 0 {
|
|
var cancel context.CancelFunc
|
|
extractCtx, cancel = context.WithTimeout(context.Background(), options.Timeout)
|
|
defer cancel()
|
|
log.Debug().Str("file", options.Path).Dur("timeout", options.Timeout).Msg("Applying timeout")
|
|
} else {
|
|
extractCtx = context.Background()
|
|
}
|
|
|
|
chapter, err := cbz.ExtractChapter(extractCtx, options.Path)
|
|
if err != nil {
|
|
log.Error().Str("file", options.Path).Err(err).Msg("Failed to extract chapter")
|
|
return fmt.Errorf("failed to extract chapter: %w", err)
|
|
}
|
|
defer func() {
|
|
if cleanupErr := chapter.Cleanup(); cleanupErr != nil {
|
|
log.Warn().Str("file", options.Path).Err(cleanupErr).Msg("Failed to cleanup temp directory")
|
|
}
|
|
}()
|
|
|
|
// Double-check conversion status from extracted metadata
|
|
if chapter.IsConverted {
|
|
log.Info().Str("file", options.Path).Msg("Chapter already converted")
|
|
return nil
|
|
}
|
|
|
|
log.Debug().
|
|
Str("file", options.Path).
|
|
Int("pages", len(chapter.Pages)).
|
|
Msg("Chapter extracted successfully")
|
|
|
|
// Step 3: Convert pages file-to-file
|
|
convertedChapter, err := options.ChapterConverter.ConvertChapter(extractCtx, chapter, options.Quality, options.Split, func(msg string, current uint32, total uint32) {
|
|
if current%10 == 0 || current == total {
|
|
log.Info().Str("file", chapter.FilePath).Uint32("current", current).Uint32("total", total).Msg("Converting")
|
|
} else {
|
|
log.Debug().Str("file", chapter.FilePath).Uint32("current", current).Uint32("total", total).Msg("Converting page")
|
|
}
|
|
})
|
|
if err != nil {
|
|
var pageIgnoredError *errors2.PageIgnoredError
|
|
if errors.As(err, &pageIgnoredError) {
|
|
log.Debug().Str("file", chapter.FilePath).Err(err).Msg("Page conversion error (non-fatal)")
|
|
} else {
|
|
log.Error().Str("file", chapter.FilePath).Err(err).Msg("Chapter conversion failed")
|
|
return fmt.Errorf("failed to convert chapter: %w", err)
|
|
}
|
|
}
|
|
if convertedChapter == nil {
|
|
log.Error().Str("file", chapter.FilePath).Msg("Conversion returned nil chapter")
|
|
return fmt.Errorf("failed to convert chapter")
|
|
}
|
|
|
|
log.Debug().
|
|
Str("file", chapter.FilePath).
|
|
Int("converted_pages", len(convertedChapter.Pages)).
|
|
Msg("Chapter conversion completed")
|
|
|
|
convertedChapter.SetConverted()
|
|
|
|
// Step 4: Determine output path
|
|
outputPath := options.Path
|
|
originalPath := options.Path
|
|
isCbrOverride := false
|
|
|
|
if options.Override {
|
|
pathLower := strings.ToLower(options.Path)
|
|
if strings.HasSuffix(pathLower, ".cbr") {
|
|
outputPath = strings.TrimSuffix(options.Path, filepath.Ext(options.Path)) + ".cbz"
|
|
isCbrOverride = true
|
|
}
|
|
} else {
|
|
pathLower := strings.ToLower(options.Path)
|
|
if strings.HasSuffix(pathLower, ".cbz") {
|
|
outputPath = strings.TrimSuffix(options.Path, ".cbz") + "_converted.cbz"
|
|
} else if strings.HasSuffix(pathLower, ".cbr") {
|
|
outputPath = strings.TrimSuffix(options.Path, ".cbr") + "_converted.cbz"
|
|
} else {
|
|
outputPath = options.Path + "_converted.cbz"
|
|
}
|
|
}
|
|
|
|
// Step 5: Write converted chapter to CBZ (streaming from disk)
|
|
log.Debug().Str("output_path", outputPath).Msg("Writing converted chapter to CBZ file")
|
|
err = cbz.WriteChapterToCBZ(convertedChapter, outputPath)
|
|
if err != nil {
|
|
log.Error().Str("output_path", outputPath).Err(err).Msg("Failed to write converted chapter")
|
|
return fmt.Errorf("failed to write converted chapter: %w", err)
|
|
}
|
|
|
|
// If overriding a CBR file, delete the original
|
|
if isCbrOverride {
|
|
err = os.Remove(originalPath)
|
|
if err != nil {
|
|
log.Warn().Str("file", originalPath).Err(err).Msg("Failed to delete original CBR file")
|
|
} else {
|
|
log.Info().Str("file", originalPath).Msg("Deleted original CBR file")
|
|
}
|
|
}
|
|
|
|
log.Info().Str("output", outputPath).Msg("Converted file written")
|
|
return nil
|
|
}
|