4 Commits

Author SHA1 Message Date
Antoine Aflalo
839ad9ed9d fix(cbz): make pages be the first in the cbz by only be number 2024-08-28 09:16:19 -04:00
Antoine Aflalo
c8879349e1 feat(split): Make the split configurable for the watch command 2024-08-28 09:10:08 -04:00
Antoine Aflalo
5ac59a93c5 feat(split): Make the split configurable for the optimize command 2024-08-28 09:06:49 -04:00
Antoine Aflalo
72c6776793 fix: make the progress more readable 2024-08-27 20:42:26 -04:00
9 changed files with 78 additions and 39 deletions

View File

@@ -30,10 +30,10 @@ func WriteChapterToCBZ(chapter *manga.Chapter, outputFilePath string) error {
var fileName string var fileName string
if page.IsSplitted { if page.IsSplitted {
// Use the format page%03d-%02d for split pages // Use the format page%03d-%02d for split pages
fileName = fmt.Sprintf("page_%04d-%02d%s", page.Index, page.SplitPartIndex, page.Extension) fileName = fmt.Sprintf("%04d-%02d%s", page.Index, page.SplitPartIndex, page.Extension)
} else { } else {
// Use the format page%03d for non-split pages // Use the format page%03d for non-split pages
fileName = fmt.Sprintf("page_%04d%s", page.Index, page.Extension) fileName = fmt.Sprintf("%04d%s", page.Index, page.Extension)
} }
// Create a new file in the ZIP archive // Create a new file in the ZIP archive

View File

@@ -31,7 +31,7 @@ func TestWriteChapterToCBZ(t *testing.T) {
IsConverted: true, IsConverted: true,
ConvertedTime: time.Now(), ConvertedTime: time.Now(),
}, },
expectedFiles: []string{"page_0000.jpg", "ComicInfo.xml", "Converted.txt"}, expectedFiles: []string{"0000.jpg", "ComicInfo.xml", "Converted.txt"},
}, },
//test case where there is only one page and no //test case where there is only one page and no
{ {
@@ -45,7 +45,7 @@ func TestWriteChapterToCBZ(t *testing.T) {
}, },
}, },
}, },
expectedFiles: []string{"page_0000.jpg"}, expectedFiles: []string{"0000.jpg"},
}, },
{ {
name: "Multiple pages with ComicInfo", name: "Multiple pages with ComicInfo",
@@ -64,7 +64,7 @@ func TestWriteChapterToCBZ(t *testing.T) {
}, },
ComicInfoXml: "<Series>Boundless Necromancer</Series>", ComicInfoXml: "<Series>Boundless Necromancer</Series>",
}, },
expectedFiles: []string{"page_0000.jpg", "page_0001.jpg", "ComicInfo.xml"}, expectedFiles: []string{"0000.jpg", "0001.jpg", "ComicInfo.xml"},
}, },
{ {
name: "Split page", name: "Split page",
@@ -79,7 +79,7 @@ func TestWriteChapterToCBZ(t *testing.T) {
}, },
}, },
}, },
expectedFiles: []string{"page_0000-01.jpg"}, expectedFiles: []string{"0000-01.jpg"},
}, },
} }

View File

@@ -29,6 +29,7 @@ func init() {
command.Flags().Uint8P("quality", "q", 85, "Quality for conversion (0-100)") 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().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("override", "o", false, "Override the original CBZ files")
command.Flags().BoolP("split", "s", false, "Split long pages into smaller chunks")
command.PersistentFlags().VarP( command.PersistentFlags().VarP(
formatFlag, formatFlag,
"format", "f", "format", "f",
@@ -58,6 +59,11 @@ func ConvertCbzCommand(cmd *cobra.Command, args []string) error {
return fmt.Errorf("invalid quality value") 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") parallelism, err := cmd.Flags().GetInt("parallelism")
if err != nil || parallelism < 1 { if err != nil || parallelism < 1 {
return fmt.Errorf("invalid parallelism value") return fmt.Errorf("invalid parallelism value")
@@ -86,7 +92,13 @@ func ConvertCbzCommand(cmd *cobra.Command, args []string) error {
go func() { go func() {
defer wg.Done() defer wg.Done()
for path := range fileChan { for path := range fileChan {
err := utils.Optimize(chapterConverter, path, quality, override) err := utils.Optimize(&utils.OptimizeOptions{
ChapterConverter: chapterConverter,
Path: path,
Quality: quality,
Override: override,
Split: split,
})
if err != nil { if err != nil {
errorChan <- fmt.Errorf("error processing file %s: %w", path, err) errorChan <- fmt.Errorf("error processing file %s: %w", path, err)
} }

View File

@@ -17,17 +17,16 @@ import (
// MockConverter is a mock implementation of the Converter interface // MockConverter is a mock implementation of the Converter interface
type MockConverter struct{} type MockConverter struct{}
func (m *MockConverter) Format() constant.ConversionFormat { func (m *MockConverter) ConvertChapter(chapter *manga.Chapter, quality uint8, split bool, progress func(message string, current uint32, total uint32)) (*manga.Chapter, error) {
return constant.WebP
}
func (m *MockConverter) ConvertChapter(chapter *manga.Chapter, quality uint8, progress func(string)) (*manga.Chapter, error) {
// Simulate conversion by setting the IsConverted flag
chapter.IsConverted = true chapter.IsConverted = true
chapter.ConvertedTime = time.Now() chapter.ConvertedTime = time.Now()
return chapter, nil return chapter, nil
} }
func (m *MockConverter) Format() constant.ConversionFormat {
return constant.WebP
}
func (m *MockConverter) PrepareConverter() error { func (m *MockConverter) PrepareConverter() error {
return nil return nil
} }
@@ -79,6 +78,7 @@ func TestConvertCbzCommand(t *testing.T) {
cmd.Flags().Uint8P("quality", "q", 85, "Quality for conversion (0-100)") 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().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("override", "o", false, "Override the original CBZ files")
cmd.Flags().BoolP("split", "s", false, "Split long pages into smaller chunks")
// Execute the command // Execute the command
err = ConvertCbzCommand(cmd, []string{tempDir}) err = ConvertCbzCommand(cmd, []string{tempDir})

View File

@@ -35,6 +35,9 @@ func init() {
command.Flags().BoolP("override", "o", true, "Override the original CBZ files") command.Flags().BoolP("override", "o", true, "Override the original CBZ files")
_ = viper.BindPFlag("override", command.Flags().Lookup("override")) _ = 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( command.PersistentFlags().VarP(
formatFlag, formatFlag,
"format", "f", "format", "f",
@@ -61,6 +64,8 @@ func WatchCommand(_ *cobra.Command, args []string) error {
override := viper.GetBool("override") override := viper.GetBool("override")
split := viper.GetBool("split")
converterType := constant.FindConversionFormat(viper.GetString("format")) converterType := constant.FindConversionFormat(viper.GetString("format"))
chapterConverter, err := converter.Get(converterType) chapterConverter, err := converter.Get(converterType)
if err != nil { if err != nil {
@@ -109,7 +114,13 @@ func WatchCommand(_ *cobra.Command, args []string) error {
for _, e := range event.Events { for _, e := range event.Events {
switch e { switch e {
case inotifywaitgo.CLOSE_WRITE, inotifywaitgo.MOVE: case inotifywaitgo.CLOSE_WRITE, inotifywaitgo.MOVE:
err := utils.Optimize(chapterConverter, event.Filename, quality, override) err := utils.Optimize(&utils.OptimizeOptions{
ChapterConverter: chapterConverter,
Path: path,
Quality: quality,
Override: override,
Split: split,
})
if err != nil { if err != nil {
errors <- fmt.Errorf("error processing file %s: %w", event.Filename, err) errors <- fmt.Errorf("error processing file %s: %w", event.Filename, err)
} }

View File

@@ -12,7 +12,7 @@ import (
type Converter interface { type Converter interface {
// Format of the converter // Format of the converter
Format() (format constant.ConversionFormat) Format() (format constant.ConversionFormat)
ConvertChapter(chapter *manga.Chapter, quality uint8, progress func(string)) (*manga.Chapter, error) ConvertChapter(chapter *manga.Chapter, quality uint8, split bool, progress func(message string, current uint32, total uint32)) (*manga.Chapter, error)
PrepareConverter() error PrepareConverter() error
} }

View File

@@ -14,18 +14,22 @@ func TestConvertChapter(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
genTestChapter func(path string) (*manga.Chapter, error) genTestChapter func(path string) (*manga.Chapter, error)
split bool
}{ }{
{ {
name: "All split pages", name: "All split pages",
genTestChapter: genBigPages, genTestChapter: genBigPages,
split: true,
}, },
{ {
name: "No split pages", name: "No split pages",
genTestChapter: genSmallPages, genTestChapter: genSmallPages,
split: false,
}, },
{ {
name: "Mix of split and no split pages", name: "Mix of split and no split pages",
genTestChapter: genMixSmallBig, genTestChapter: genMixSmallBig,
split: true,
}, },
} }
// Load test genTestChapter from testdata // Load test genTestChapter from testdata
@@ -50,11 +54,11 @@ func TestConvertChapter(t *testing.T) {
quality := uint8(80) quality := uint8(80)
progress := func(msg string) { progress := func(msg string, current uint32, total uint32) {
t.Log(msg) t.Log(msg)
} }
convertedChapter, err := converter.ConvertChapter(chapter, quality, progress) convertedChapter, err := converter.ConvertChapter(chapter, quality, false, progress)
if err != nil { if err != nil {
t.Fatalf("failed to convert genTestChapter: %v", err) t.Fatalf("failed to convert genTestChapter: %v", err)
} }

View File

@@ -4,7 +4,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"github.com/belphemur/CBZOptimizer/converter/constant" "github.com/belphemur/CBZOptimizer/converter/constant"
packer2 "github.com/belphemur/CBZOptimizer/manga" "github.com/belphemur/CBZOptimizer/manga"
"github.com/oliamb/cutter" "github.com/oliamb/cutter"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
_ "golang.org/x/image/webp" _ "golang.org/x/image/webp"
@@ -48,7 +48,7 @@ func (converter *Converter) PrepareConverter() error {
return nil return nil
} }
func (converter *Converter) ConvertChapter(chapter *packer2.Chapter, quality uint8, progress func(string)) (*packer2.Chapter, error) { func (converter *Converter) ConvertChapter(chapter *manga.Chapter, quality uint8, split bool, progress func(message string, current uint32, total uint32)) (*manga.Chapter, error) {
err := converter.PrepareConverter() err := converter.PrepareConverter()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -57,7 +57,7 @@ func (converter *Converter) ConvertChapter(chapter *packer2.Chapter, quality uin
var wgConvertedPages sync.WaitGroup var wgConvertedPages sync.WaitGroup
maxGoroutines := runtime.NumCPU() maxGoroutines := runtime.NumCPU()
pagesChan := make(chan *packer2.PageContainer, maxGoroutines) pagesChan := make(chan *manga.PageContainer, maxGoroutines)
errChan := make(chan error, maxGoroutines) errChan := make(chan error, maxGoroutines)
var wgPages sync.WaitGroup var wgPages sync.WaitGroup
@@ -65,13 +65,13 @@ func (converter *Converter) ConvertChapter(chapter *packer2.Chapter, quality uin
guard := make(chan struct{}, maxGoroutines) guard := make(chan struct{}, maxGoroutines)
pagesMutex := sync.Mutex{} pagesMutex := sync.Mutex{}
var pages []*packer2.Page var pages []*manga.Page
var totalPages = uint32(len(chapter.Pages)) var totalPages = uint32(len(chapter.Pages))
go func() { go func() {
for page := range pagesChan { for page := range pagesChan {
guard <- struct{}{} // would block if guard channel is already filled guard <- struct{}{} // would block if guard channel is already filled
go func(pageToConvert *packer2.PageContainer) { go func(pageToConvert *manga.PageContainer) {
defer wgConvertedPages.Done() defer wgConvertedPages.Done()
convertedPage, err := converter.convertPage(pageToConvert, quality) convertedPage, err := converter.convertPage(pageToConvert, quality)
if err != nil { if err != nil {
@@ -93,7 +93,7 @@ func (converter *Converter) ConvertChapter(chapter *packer2.Chapter, quality uin
} }
pagesMutex.Lock() pagesMutex.Lock()
pages = append(pages, convertedPage.Page) pages = append(pages, convertedPage.Page)
progress(fmt.Sprintf("Converted %d/%d pages to %s format", len(pages), totalPages, converter.Format())) progress(fmt.Sprintf("Converted %d/%d pages to %s format", len(pages), totalPages, converter.Format()), uint32(len(pages)), totalPages)
pagesMutex.Unlock() pagesMutex.Unlock()
<-guard <-guard
}(page) }(page)
@@ -101,10 +101,12 @@ func (converter *Converter) ConvertChapter(chapter *packer2.Chapter, quality uin
}() }()
for _, page := range chapter.Pages { for _, page := range chapter.Pages {
go func(page *packer2.Page) { go func(page *manga.Page) {
defer wgPages.Done() defer wgPages.Done()
splitNeeded, img, format, err := converter.checkPageNeedsSplit(page) splitNeeded, img, format, err := converter.checkPageNeedsSplit(page)
// Respect choice to split or not
splitNeeded = split && splitNeeded
if err != nil { if err != nil {
errChan <- fmt.Errorf("error checking if page %d of genTestChapter %s needs split: %v", page.Index, chapter.FilePath, err) errChan <- fmt.Errorf("error checking if page %d of genTestChapter %s needs split: %v", page.Index, chapter.FilePath, err)
return return
@@ -112,7 +114,7 @@ func (converter *Converter) ConvertChapter(chapter *packer2.Chapter, quality uin
if !splitNeeded { if !splitNeeded {
wgConvertedPages.Add(1) wgConvertedPages.Add(1)
pagesChan <- packer2.NewContainer(page, img, format) pagesChan <- manga.NewContainer(page, img, format)
return return
} }
images, err := converter.cropImage(img) images, err := converter.cropImage(img)
@@ -123,9 +125,9 @@ func (converter *Converter) ConvertChapter(chapter *packer2.Chapter, quality uin
atomic.AddUint32(&totalPages, uint32(len(images)-1)) atomic.AddUint32(&totalPages, uint32(len(images)-1))
for i, img := range images { for i, img := range images {
page := &packer2.Page{Index: page.Index, IsSplitted: true, SplitPartIndex: uint16(i)} page := &manga.Page{Index: page.Index, IsSplitted: true, SplitPartIndex: uint16(i)}
wgConvertedPages.Add(1) wgConvertedPages.Add(1)
pagesChan <- packer2.NewContainer(page, img, "N/A") pagesChan <- manga.NewContainer(page, img, "N/A")
} }
}(page) }(page)
} }
@@ -144,7 +146,7 @@ func (converter *Converter) ConvertChapter(chapter *packer2.Chapter, quality uin
return nil, fmt.Errorf("encountered errors: %v", errList) return nil, fmt.Errorf("encountered errors: %v", errList)
} }
slices.SortFunc(pages, func(a, b *packer2.Page) int { slices.SortFunc(pages, func(a, b *manga.Page) int {
if a.Index == b.Index { if a.Index == b.Index {
return int(b.SplitPartIndex - a.SplitPartIndex) return int(b.SplitPartIndex - a.SplitPartIndex)
} }
@@ -190,7 +192,7 @@ func (converter *Converter) cropImage(img image.Image) ([]image.Image, error) {
return parts, nil return parts, nil
} }
func (converter *Converter) checkPageNeedsSplit(page *packer2.Page) (bool, image.Image, string, error) { func (converter *Converter) checkPageNeedsSplit(page *manga.Page) (bool, image.Image, string, error) {
reader := io.Reader(bytes.NewBuffer(page.Contents.Bytes())) reader := io.Reader(bytes.NewBuffer(page.Contents.Bytes()))
img, format, err := image.Decode(reader) img, format, err := image.Decode(reader)
if err != nil { if err != nil {
@@ -203,7 +205,7 @@ func (converter *Converter) checkPageNeedsSplit(page *packer2.Page) (bool, image
return height >= converter.maxHeight, img, format, nil return height >= converter.maxHeight, img, format, nil
} }
func (converter *Converter) convertPage(container *packer2.PageContainer, quality uint8) (*packer2.PageContainer, error) { func (converter *Converter) convertPage(container *manga.PageContainer, quality uint8) (*manga.PageContainer, error) {
if container.Format == "webp" { if container.Format == "webp" {
return container, nil return container, nil
} }

View File

@@ -8,24 +8,34 @@ import (
"strings" "strings"
) )
type OptimizeOptions struct {
ChapterConverter converter.Converter
Path string
Quality uint8
Override bool
Split bool
}
// Optimize optimizes a CBZ file using the specified converter. // Optimize optimizes a CBZ file using the specified converter.
func Optimize(chapterConverter converter.Converter, path string, quality uint8, override bool) error { func Optimize(options *OptimizeOptions) error {
log.Printf("Processing file: %s\n", path) log.Printf("Processing file: %s\n", options.Path)
// Load the chapter // Load the chapter
chapter, err := cbz.LoadChapter(path) chapter, err := cbz.LoadChapter(options.Path)
if err != nil { if err != nil {
return fmt.Errorf("failed to load chapter: %v", err) return fmt.Errorf("failed to load chapter: %v", err)
} }
if chapter.IsConverted { if chapter.IsConverted {
log.Printf("Chapter already converted: %s", path) log.Printf("Chapter already converted: %s", options.Path)
return nil return nil
} }
// Convert the chapter // Convert the chapter
convertedChapter, err := chapterConverter.ConvertChapter(chapter, quality, func(msg string) { convertedChapter, err := options.ChapterConverter.ConvertChapter(chapter, options.Quality, options.Split, func(msg string, current uint32, total uint32) {
log.Printf("[%s]%s", path, msg) if current%10 == 0 || current == total {
log.Printf("[%s] Converting: %d/%d", chapter.FilePath, current, total)
}
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to convert chapter: %v", err) return fmt.Errorf("failed to convert chapter: %v", err)
@@ -33,9 +43,9 @@ func Optimize(chapterConverter converter.Converter, path string, quality uint8,
convertedChapter.SetConverted() convertedChapter.SetConverted()
// Write the converted chapter back to a CBZ file // Write the converted chapter back to a CBZ file
outputPath := path outputPath := options.Path
if !override { if !options.Override {
outputPath = strings.TrimSuffix(path, ".cbz") + "_converted.cbz" outputPath = strings.TrimSuffix(options.Path, ".cbz") + "_converted.cbz"
} }
err = cbz.WriteChapterToCBZ(convertedChapter, outputPath) err = cbz.WriteChapterToCBZ(convertedChapter, outputPath)
if err != nil { if err != nil {