mirror of
https://github.com/Belphemur/CBZOptimizer.git
synced 2025-10-14 04:28:51 +02:00
refactor: update import paths to use internal package
This commit is contained in:
140
cmd/cbzoptimizer/commands/optimize_command.go
Normal file
140
cmd/cbzoptimizer/commands/optimize_command.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
utils2 "github.com/belphemur/CBZOptimizer/v2/internal/utils"
|
||||
"github.com/belphemur/CBZOptimizer/v2/pkg/converter"
|
||||
"github.com/belphemur/CBZOptimizer/v2/pkg/converter/constant"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/thediveo/enumflag/v2"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var converterType constant.ConversionFormat
|
||||
|
||||
func init() {
|
||||
command := &cobra.Command{
|
||||
Use: "optimize [folder]",
|
||||
Short: "Optimize all CBZ files in a folder recursively",
|
||||
Long: "Optimize all CBZ files in a folder recursively.\nIt will take all the different pages in the CBZ files and convert them to the given format.\nThe original CBZ files will be kept intact depending if you choose to override or not.",
|
||||
RunE: ConvertCbzCommand,
|
||||
Args: cobra.ExactArgs(1),
|
||||
}
|
||||
formatFlag := enumflag.New(&converterType, "format", constant.CommandValue, enumflag.EnumCaseInsensitive)
|
||||
_ = formatFlag.RegisterCompletion(command, "format", constant.HelpText)
|
||||
|
||||
command.Flags().Uint8P("quality", "q", 85, "Quality for conversion (0-100)")
|
||||
command.Flags().IntP("parallelism", "n", 2, "Number of chapters to convert in parallel")
|
||||
command.Flags().BoolP("override", "o", false, "Override the original CBZ files")
|
||||
command.Flags().BoolP("split", "s", false, "Split long pages into smaller chunks")
|
||||
command.PersistentFlags().VarP(
|
||||
formatFlag,
|
||||
"format", "f",
|
||||
fmt.Sprintf("Format to convert the images to: %s", constant.ListAll()))
|
||||
command.PersistentFlags().Lookup("format").NoOptDefVal = constant.DefaultConversion.String()
|
||||
|
||||
AddCommand(command)
|
||||
}
|
||||
|
||||
func ConvertCbzCommand(cmd *cobra.Command, args []string) error {
|
||||
path := args[0]
|
||||
if path == "" {
|
||||
return fmt.Errorf("path is required")
|
||||
}
|
||||
|
||||
if !utils2.IsValidFolder(path) {
|
||||
return fmt.Errorf("the path needs to be a folder")
|
||||
}
|
||||
|
||||
quality, err := cmd.Flags().GetUint8("quality")
|
||||
if err != nil || quality <= 0 || quality > 100 {
|
||||
return fmt.Errorf("invalid quality value")
|
||||
}
|
||||
|
||||
override, err := cmd.Flags().GetBool("override")
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid quality value")
|
||||
}
|
||||
|
||||
split, err := cmd.Flags().GetBool("split")
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid split value")
|
||||
}
|
||||
|
||||
parallelism, err := cmd.Flags().GetInt("parallelism")
|
||||
if err != nil || parallelism < 1 {
|
||||
return fmt.Errorf("invalid parallelism value")
|
||||
}
|
||||
|
||||
chapterConverter, err := converter.Get(converterType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get chapterConverter: %v", err)
|
||||
}
|
||||
|
||||
err = chapterConverter.PrepareConverter()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prepare converter: %v", err)
|
||||
}
|
||||
// Channel to manage the files to process
|
||||
fileChan := make(chan string)
|
||||
// Channel to collect errors
|
||||
errorChan := make(chan error, parallelism)
|
||||
|
||||
// WaitGroup to wait for all goroutines to finish
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Start worker goroutines
|
||||
for i := 0; i < parallelism; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for path := range fileChan {
|
||||
err := utils2.Optimize(&utils2.OptimizeOptions{
|
||||
ChapterConverter: chapterConverter,
|
||||
Path: path,
|
||||
Quality: quality,
|
||||
Override: override,
|
||||
Split: split,
|
||||
})
|
||||
if err != nil {
|
||||
errorChan <- fmt.Errorf("error processing file %s: %w", path, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Walk the path and send files to the channel
|
||||
err = filepath.WalkDir(path, func(path string, info os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".cbz") {
|
||||
fileChan <- path
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error walking the path: %w", err)
|
||||
}
|
||||
|
||||
close(fileChan) // Close the channel to signal workers to stop
|
||||
wg.Wait() // Wait for all workers to finish
|
||||
close(errorChan) // Close the error channel
|
||||
|
||||
var errs []error
|
||||
for err := range errorChan {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("encountered errors: %v", errs)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
122
cmd/cbzoptimizer/commands/optimize_command_test.go
Normal file
122
cmd/cbzoptimizer/commands/optimize_command_test.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/belphemur/CBZOptimizer/v2/internal/cbz"
|
||||
"github.com/belphemur/CBZOptimizer/v2/internal/manga"
|
||||
"github.com/belphemur/CBZOptimizer/v2/internal/utils/errs"
|
||||
"github.com/belphemur/CBZOptimizer/v2/pkg/converter"
|
||||
"github.com/belphemur/CBZOptimizer/v2/pkg/converter/constant"
|
||||
"github.com/spf13/cobra"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MockConverter is a mock implementation of the Converter interface
|
||||
type MockConverter struct{}
|
||||
|
||||
func (m *MockConverter) ConvertChapter(chapter *manga.Chapter, quality uint8, split bool, progress func(message string, current uint32, total uint32)) (*manga.Chapter, error) {
|
||||
chapter.IsConverted = true
|
||||
chapter.ConvertedTime = time.Now()
|
||||
return chapter, nil
|
||||
}
|
||||
|
||||
func (m *MockConverter) Format() constant.ConversionFormat {
|
||||
return constant.WebP
|
||||
}
|
||||
|
||||
func (m *MockConverter) PrepareConverter() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestConvertCbzCommand(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
tempDir, err := os.MkdirTemp("", "test_cbz")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer errs.CaptureGeneric(&err, os.RemoveAll, tempDir, "failed to remove temporary directory")
|
||||
|
||||
// Locate the testdata directory
|
||||
testdataDir := filepath.Join("../testdata")
|
||||
if _, err := os.Stat(testdataDir); os.IsNotExist(err) {
|
||||
t.Fatalf("testdata directory not found")
|
||||
}
|
||||
|
||||
// Copy sample CBZ files from testdata to the temporary directory
|
||||
err = filepath.Walk(testdataDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".cbz") {
|
||||
destPath := filepath.Join(tempDir, info.Name())
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(destPath, data, info.Mode())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to copy sample files: %v", err)
|
||||
}
|
||||
|
||||
// Mock the converter.Get function
|
||||
originalGet := converter.Get
|
||||
converter.Get = func(format constant.ConversionFormat) (converter.Converter, error) {
|
||||
return &MockConverter{}, nil
|
||||
}
|
||||
defer func() { converter.Get = originalGet }()
|
||||
|
||||
// Set up the command
|
||||
cmd := &cobra.Command{
|
||||
Use: "optimize",
|
||||
}
|
||||
cmd.Flags().Uint8P("quality", "q", 85, "Quality for conversion (0-100)")
|
||||
cmd.Flags().IntP("parallelism", "n", 2, "Number of chapters to convert in parallel")
|
||||
cmd.Flags().BoolP("override", "o", false, "Override the original CBZ files")
|
||||
cmd.Flags().BoolP("split", "s", false, "Split long pages into smaller chunks")
|
||||
|
||||
// Execute the command
|
||||
err = ConvertCbzCommand(cmd, []string{tempDir})
|
||||
if err != nil {
|
||||
t.Fatalf("Command execution failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify the results
|
||||
err = filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() || !strings.HasSuffix(info.Name(), "_converted.cbz") {
|
||||
return nil
|
||||
}
|
||||
t.Logf("CBZ file found: %s", path)
|
||||
|
||||
// Load the converted chapter
|
||||
chapter, err := cbz.LoadChapter(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the chapter is marked as converted
|
||||
if !chapter.IsConverted {
|
||||
t.Errorf("Chapter is not marked as converted: %s", path)
|
||||
}
|
||||
|
||||
// Check if the ConvertedTime is set
|
||||
if chapter.ConvertedTime.IsZero() {
|
||||
t.Errorf("ConvertedTime is not set for chapter: %s", path)
|
||||
}
|
||||
t.Logf("CBZ file [%s] is converted: %s", path, chapter.ConvertedTime)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Error verifying converted files: %v", err)
|
||||
}
|
||||
}
|
62
cmd/cbzoptimizer/commands/rootcmd.go
Normal file
62
cmd/cbzoptimizer/commands/rootcmd.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "cbzconverter",
|
||||
Short: "Convert CBZ files using a specified converter",
|
||||
}
|
||||
|
||||
func SetVersionInfo(version, commit, date string) {
|
||||
rootCmd.Version = fmt.Sprintf("%s (Built on %s from Git SHA %s)", version, date, commit)
|
||||
}
|
||||
|
||||
func getPath() string {
|
||||
return filepath.Join(map[string]string{
|
||||
"windows": filepath.Join(os.Getenv("APPDATA")),
|
||||
"darwin": filepath.Join(os.Getenv("HOME"), ".config"),
|
||||
"linux": filepath.Join(os.Getenv("HOME"), ".config"),
|
||||
}[runtime.GOOS], "CBZOptimizer")
|
||||
}
|
||||
|
||||
func init() {
|
||||
configFolder := getPath()
|
||||
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath(configFolder)
|
||||
viper.SetEnvPrefix("CBZ")
|
||||
viper.AutomaticEnv()
|
||||
err := os.MkdirAll(configFolder, os.ModePerm)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("fatal error config file: %w", err))
|
||||
}
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
||||
err := viper.SafeWriteConfig()
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("fatal error config file: %w", err))
|
||||
}
|
||||
} else {
|
||||
panic(fmt.Errorf("fatal error config file: %w", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute executes the root command.
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
func AddCommand(cmd *cobra.Command) {
|
||||
rootCmd.AddCommand(cmd)
|
||||
}
|
144
cmd/cbzoptimizer/commands/watch_command.go
Normal file
144
cmd/cbzoptimizer/commands/watch_command.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
utils2 "github.com/belphemur/CBZOptimizer/v2/internal/utils"
|
||||
"github.com/belphemur/CBZOptimizer/v2/pkg/converter"
|
||||
"github.com/belphemur/CBZOptimizer/v2/pkg/converter/constant"
|
||||
"github.com/pablodz/inotifywaitgo/inotifywaitgo"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/thediveo/enumflag/v2"
|
||||
"log"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if runtime.GOOS != "linux" {
|
||||
return
|
||||
}
|
||||
command := &cobra.Command{
|
||||
Use: "watch [folder]",
|
||||
Short: "Watch a folder for new CBZ files",
|
||||
Long: "Watch a folder for new CBZ files.\nIt will watch a folder for new CBZ files and optimize them.",
|
||||
RunE: WatchCommand,
|
||||
Args: cobra.ExactArgs(1),
|
||||
}
|
||||
formatFlag := enumflag.New(&converterType, "format", constant.CommandValue, enumflag.EnumCaseInsensitive)
|
||||
_ = formatFlag.RegisterCompletion(command, "format", constant.HelpText)
|
||||
|
||||
command.Flags().Uint8P("quality", "q", 85, "Quality for conversion (0-100)")
|
||||
_ = viper.BindPFlag("quality", command.Flags().Lookup("quality"))
|
||||
|
||||
command.Flags().BoolP("override", "o", true, "Override the original CBZ files")
|
||||
_ = viper.BindPFlag("override", command.Flags().Lookup("override"))
|
||||
|
||||
command.Flags().BoolP("split", "s", false, "Split long pages into smaller chunks")
|
||||
_ = viper.BindPFlag("split", command.Flags().Lookup("split"))
|
||||
|
||||
command.PersistentFlags().VarP(
|
||||
formatFlag,
|
||||
"format", "f",
|
||||
fmt.Sprintf("Format to convert the images to: %s", constant.ListAll()))
|
||||
command.PersistentFlags().Lookup("format").NoOptDefVal = constant.DefaultConversion.String()
|
||||
_ = viper.BindPFlag("format", command.PersistentFlags().Lookup("format"))
|
||||
|
||||
AddCommand(command)
|
||||
}
|
||||
func WatchCommand(_ *cobra.Command, args []string) error {
|
||||
path := args[0]
|
||||
if path == "" {
|
||||
return fmt.Errorf("path is required")
|
||||
}
|
||||
|
||||
if !utils2.IsValidFolder(path) {
|
||||
return fmt.Errorf("the path needs to be a folder")
|
||||
}
|
||||
|
||||
quality := uint8(viper.GetUint16("quality"))
|
||||
if quality <= 0 || quality > 100 {
|
||||
return fmt.Errorf("invalid quality value")
|
||||
}
|
||||
|
||||
override := viper.GetBool("override")
|
||||
|
||||
split := viper.GetBool("split")
|
||||
|
||||
converterType := constant.FindConversionFormat(viper.GetString("format"))
|
||||
chapterConverter, err := converter.Get(converterType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get chapterConverter: %v", err)
|
||||
}
|
||||
|
||||
err = chapterConverter.PrepareConverter()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prepare converter: %v", err)
|
||||
}
|
||||
log.Printf("Watching [%s] with [override: %t, quality: %d, format: %s, split: %t]", path, override, quality, converterType.String(), split)
|
||||
|
||||
events := make(chan inotifywaitgo.FileEvent)
|
||||
errors := make(chan error)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
inotifywaitgo.WatchPath(&inotifywaitgo.Settings{
|
||||
Dir: path,
|
||||
FileEvents: events,
|
||||
ErrorChan: errors,
|
||||
Options: &inotifywaitgo.Options{
|
||||
Recursive: true,
|
||||
Events: []inotifywaitgo.EVENT{
|
||||
inotifywaitgo.MOVE,
|
||||
inotifywaitgo.CLOSE_WRITE,
|
||||
},
|
||||
Monitor: true,
|
||||
},
|
||||
Verbose: true,
|
||||
})
|
||||
}()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for event := range events {
|
||||
log.Printf("[Event]%s, %v\n", event.Filename, event.Events)
|
||||
|
||||
if !strings.HasSuffix(strings.ToLower(event.Filename), ".cbz") {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, e := range event.Events {
|
||||
switch e {
|
||||
case inotifywaitgo.CLOSE_WRITE, inotifywaitgo.MOVE:
|
||||
err := utils2.Optimize(&utils2.OptimizeOptions{
|
||||
ChapterConverter: chapterConverter,
|
||||
Path: event.Filename,
|
||||
Quality: quality,
|
||||
Override: override,
|
||||
Split: split,
|
||||
})
|
||||
if err != nil {
|
||||
errors <- fmt.Errorf("error processing file %s: %w", event.Filename, err)
|
||||
}
|
||||
default:
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for err := range errors {
|
||||
log.Printf("Error: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
16
cmd/cbzoptimizer/main.go
Normal file
16
cmd/cbzoptimizer/main.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/belphemur/CBZOptimizer/v2/cmd/cbzoptimizer/commands"
|
||||
)
|
||||
|
||||
var (
|
||||
version = "dev"
|
||||
commit = "none"
|
||||
date = "unknown"
|
||||
)
|
||||
|
||||
func main() {
|
||||
commands.SetVersionInfo(version, commit, date)
|
||||
commands.Execute()
|
||||
}
|
Reference in New Issue
Block a user