mirror of
https://github.com/Belphemur/CBZOptimizer.git
synced 2025-10-13 20:18:52 +02:00
refactor: update import paths to use internal package
This commit is contained in:
83
internal/cbz/cbz_creator.go
Normal file
83
internal/cbz/cbz_creator.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package cbz
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"github.com/belphemur/CBZOptimizer/v2/internal/manga"
|
||||
"github.com/belphemur/CBZOptimizer/v2/internal/utils/errs"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func WriteChapterToCBZ(chapter *manga.Chapter, outputFilePath string) error {
|
||||
// Create a new ZIP file
|
||||
zipFile, err := os.Create(outputFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create .cbz file: %w", err)
|
||||
}
|
||||
defer errs.Capture(&err, zipFile.Close, "failed to close .cbz file")
|
||||
|
||||
// Create a new ZIP writer
|
||||
zipWriter := zip.NewWriter(zipFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer errs.Capture(&err, zipWriter.Close, "failed to close .cbz writer")
|
||||
|
||||
// Write each page to the ZIP archive
|
||||
for _, page := range chapter.Pages {
|
||||
// Construct the file name for the page
|
||||
var fileName string
|
||||
if page.IsSplitted {
|
||||
// Use the format page%03d-%02d for split pages
|
||||
fileName = fmt.Sprintf("%04d-%02d%s", page.Index, page.SplitPartIndex, page.Extension)
|
||||
} else {
|
||||
// Use the format page%03d for non-split pages
|
||||
fileName = fmt.Sprintf("%04d%s", page.Index, page.Extension)
|
||||
}
|
||||
|
||||
// Create a new file in the ZIP archive
|
||||
fileWriter, err := zipWriter.CreateHeader(&zip.FileHeader{
|
||||
Name: fileName,
|
||||
Method: zip.Store,
|
||||
Modified: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file in .cbz: %w", err)
|
||||
}
|
||||
|
||||
// Write the page contents to the file
|
||||
_, err = fileWriter.Write(page.Contents.Bytes())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write page contents: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Optionally, write the ComicInfo.xml file if present
|
||||
if chapter.ComicInfoXml != "" {
|
||||
comicInfoWriter, err := zipWriter.CreateHeader(&zip.FileHeader{
|
||||
Name: "ComicInfo.xml",
|
||||
Method: zip.Deflate,
|
||||
Modified: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create ComicInfo.xml in .cbz: %w", err)
|
||||
}
|
||||
|
||||
_, err = comicInfoWriter.Write([]byte(chapter.ComicInfoXml))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write ComicInfo.xml contents: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if chapter.IsConverted {
|
||||
|
||||
convertedString := fmt.Sprintf("%s\nThis chapter has been converted by CBZOptimizer.", chapter.ConvertedTime)
|
||||
err = zipWriter.SetComment(convertedString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write comment: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
144
internal/cbz/cbz_creator_test.go
Normal file
144
internal/cbz/cbz_creator_test.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package cbz
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/belphemur/CBZOptimizer/v2/internal/manga"
|
||||
"github.com/belphemur/CBZOptimizer/v2/internal/utils/errs"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWriteChapterToCBZ(t *testing.T) {
|
||||
currentTime := time.Now()
|
||||
|
||||
// Define test cases
|
||||
testCases := []struct {
|
||||
name string
|
||||
chapter *manga.Chapter
|
||||
expectedFiles []string
|
||||
expectedComment string
|
||||
}{
|
||||
//test case where there is only one page and ComicInfo and the chapter is converted
|
||||
{
|
||||
name: "Single page, ComicInfo, converted",
|
||||
chapter: &manga.Chapter{
|
||||
Pages: []*manga.Page{
|
||||
{
|
||||
Index: 0,
|
||||
Extension: ".jpg",
|
||||
Contents: bytes.NewBuffer([]byte("image data")),
|
||||
},
|
||||
},
|
||||
ComicInfoXml: "<Series>Boundless Necromancer</Series>",
|
||||
IsConverted: true,
|
||||
ConvertedTime: currentTime,
|
||||
},
|
||||
expectedFiles: []string{"0000.jpg", "ComicInfo.xml"},
|
||||
expectedComment: fmt.Sprintf("%s\nThis chapter has been converted by CBZOptimizer.", currentTime),
|
||||
},
|
||||
//test case where there is only one page and no
|
||||
{
|
||||
name: "Single page, no ComicInfo",
|
||||
chapter: &manga.Chapter{
|
||||
Pages: []*manga.Page{
|
||||
{
|
||||
Index: 0,
|
||||
Extension: ".jpg",
|
||||
Contents: bytes.NewBuffer([]byte("image data")),
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedFiles: []string{"0000.jpg"},
|
||||
},
|
||||
{
|
||||
name: "Multiple pages with ComicInfo",
|
||||
chapter: &manga.Chapter{
|
||||
Pages: []*manga.Page{
|
||||
{
|
||||
Index: 0,
|
||||
Extension: ".jpg",
|
||||
Contents: bytes.NewBuffer([]byte("image data 1")),
|
||||
},
|
||||
{
|
||||
Index: 1,
|
||||
Extension: ".jpg",
|
||||
Contents: bytes.NewBuffer([]byte("image data 2")),
|
||||
},
|
||||
},
|
||||
ComicInfoXml: "<Series>Boundless Necromancer</Series>",
|
||||
},
|
||||
expectedFiles: []string{"0000.jpg", "0001.jpg", "ComicInfo.xml"},
|
||||
},
|
||||
{
|
||||
name: "Split page",
|
||||
chapter: &manga.Chapter{
|
||||
Pages: []*manga.Page{
|
||||
{
|
||||
Index: 0,
|
||||
Extension: ".jpg",
|
||||
Contents: bytes.NewBuffer([]byte("split image data")),
|
||||
IsSplitted: true,
|
||||
SplitPartIndex: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedFiles: []string{"0000-01.jpg"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Create a temporary file for the .cbz output
|
||||
tempFile, err := os.CreateTemp("", "*.cbz")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temporary file: %v", err)
|
||||
}
|
||||
defer errs.CaptureGeneric(&err, os.Remove, tempFile.Name(), "failed to remove temporary file")
|
||||
|
||||
// Write the chapter to the .cbz file
|
||||
err = WriteChapterToCBZ(tc.chapter, tempFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write chapter to CBZ: %v", err)
|
||||
}
|
||||
|
||||
// Open the .cbz file as a zip archive
|
||||
r, err := zip.OpenReader(tempFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open CBZ file: %v", err)
|
||||
}
|
||||
defer errs.Capture(&err, r.Close, "failed to close CBZ file")
|
||||
|
||||
// Collect the names of the files in the archive
|
||||
var filesInArchive []string
|
||||
for _, f := range r.File {
|
||||
filesInArchive = append(filesInArchive, f.Name)
|
||||
}
|
||||
|
||||
// Check if all expected files are present
|
||||
for _, expectedFile := range tc.expectedFiles {
|
||||
found := false
|
||||
for _, actualFile := range filesInArchive {
|
||||
if actualFile == expectedFile {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected file %s not found in archive", expectedFile)
|
||||
}
|
||||
}
|
||||
|
||||
if tc.expectedComment != "" && r.Comment != tc.expectedComment {
|
||||
t.Errorf("Expected comment %s, but found %s", tc.expectedComment, r.Comment)
|
||||
}
|
||||
|
||||
// Check if there are no unexpected files
|
||||
if len(filesInArchive) != len(tc.expectedFiles) {
|
||||
t.Errorf("Expected %d files, but found %d", len(tc.expectedFiles), len(filesInArchive))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
104
internal/cbz/cbz_loader.go
Normal file
104
internal/cbz/cbz_loader.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package cbz
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/araddon/dateparse"
|
||||
"github.com/belphemur/CBZOptimizer/v2/internal/manga"
|
||||
"github.com/belphemur/CBZOptimizer/v2/internal/utils/errs"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func LoadChapter(filePath string) (*manga.Chapter, error) {
|
||||
// Open the .cbz file
|
||||
r, err := zip.OpenReader(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open .cbz file: %w", err)
|
||||
}
|
||||
defer errs.Capture(&err, r.Close, "failed to close opened .cbz file")
|
||||
|
||||
chapter := &manga.Chapter{
|
||||
FilePath: filePath,
|
||||
}
|
||||
// Check for comment
|
||||
if r.Comment != "" {
|
||||
scanner := bufio.NewScanner(strings.NewReader(r.Comment))
|
||||
if scanner.Scan() {
|
||||
convertedTime := scanner.Text()
|
||||
chapter.ConvertedTime, err = dateparse.ParseAny(convertedTime)
|
||||
if err == nil {
|
||||
chapter.IsConverted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, f := range r.File {
|
||||
if f.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
err := func() error {
|
||||
// Open the file inside the zip
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file inside .cbz: %w", err)
|
||||
}
|
||||
|
||||
defer errs.Capture(&err, rc.Close, "failed to close file inside .cbz")
|
||||
|
||||
// Determine the file extension
|
||||
ext := strings.ToLower(filepath.Ext(f.Name))
|
||||
|
||||
if ext == ".xml" && strings.ToLower(filepath.Base(f.Name)) == "comicinfo.xml" {
|
||||
// Read the ComicInfo.xml file content
|
||||
xmlContent, err := io.ReadAll(rc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read ComicInfo.xml content: %w", err)
|
||||
}
|
||||
chapter.ComicInfoXml = string(xmlContent)
|
||||
} else if !chapter.IsConverted && ext == ".txt" && strings.ToLower(filepath.Base(f.Name)) == "converted.txt" {
|
||||
textContent, err := io.ReadAll(rc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read Converted.xml content: %w", err)
|
||||
}
|
||||
scanner := bufio.NewScanner(bytes.NewReader(textContent))
|
||||
if scanner.Scan() {
|
||||
convertedTime := scanner.Text()
|
||||
chapter.ConvertedTime, err = dateparse.ParseAny(convertedTime)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse converted time: %w", err)
|
||||
}
|
||||
chapter.IsConverted = true
|
||||
}
|
||||
} else {
|
||||
// Read the file contents for page
|
||||
buf := new(bytes.Buffer)
|
||||
_, err = io.Copy(buf, rc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file contents: %w", err)
|
||||
}
|
||||
|
||||
// Create a new Page object
|
||||
page := &manga.Page{
|
||||
Index: uint16(len(chapter.Pages)), // Simple index based on order
|
||||
Extension: ext,
|
||||
Size: uint64(buf.Len()),
|
||||
Contents: buf,
|
||||
IsSplitted: false,
|
||||
}
|
||||
|
||||
// Add the page to the chapter
|
||||
chapter.Pages = append(chapter.Pages, page)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return chapter, nil
|
||||
}
|
56
internal/cbz/cbz_loader_test.go
Normal file
56
internal/cbz/cbz_loader_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package cbz
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadChapter(t *testing.T) {
|
||||
type testCase struct {
|
||||
name string
|
||||
filePath string
|
||||
expectedPages int
|
||||
expectedSeries string
|
||||
expectedConversion bool
|
||||
}
|
||||
|
||||
testCases := []testCase{
|
||||
{
|
||||
name: "Original Chapter",
|
||||
filePath: "../testdata/Chapter 1.cbz",
|
||||
expectedPages: 16,
|
||||
expectedSeries: "<Series>Boundless Necromancer</Series>",
|
||||
expectedConversion: false,
|
||||
},
|
||||
{
|
||||
name: "Converted Chapter",
|
||||
filePath: "../testdata/Chapter 10_converted.cbz",
|
||||
expectedPages: 107,
|
||||
expectedSeries: "<Series>Boundless Necromancer</Series>",
|
||||
expectedConversion: true,
|
||||
},
|
||||
// Add more test cases as needed
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
chapter, err := LoadChapter(tc.filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load chapter: %v", err)
|
||||
}
|
||||
|
||||
actualPages := len(chapter.Pages)
|
||||
if actualPages != tc.expectedPages {
|
||||
t.Errorf("Expected %d pages, but got %d", tc.expectedPages, actualPages)
|
||||
}
|
||||
|
||||
if !strings.Contains(chapter.ComicInfoXml, tc.expectedSeries) {
|
||||
t.Errorf("ComicInfoXml does not contain the expected series: %s", tc.expectedSeries)
|
||||
}
|
||||
|
||||
if chapter.IsConverted != tc.expectedConversion {
|
||||
t.Errorf("Expected chapter to be converted: %t, but got %t", tc.expectedConversion, chapter.IsConverted)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
22
internal/manga/chapter.go
Normal file
22
internal/manga/chapter.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package manga
|
||||
|
||||
import "time"
|
||||
|
||||
type Chapter struct {
|
||||
// FilePath is the path to the chapter's directory.
|
||||
FilePath string
|
||||
// Pages is a slice of pointers to Page objects.
|
||||
Pages []*Page
|
||||
// ComicInfo is a string containing information about the chapter.
|
||||
ComicInfoXml string
|
||||
// IsConverted is a boolean that indicates whether the chapter has been converted.
|
||||
IsConverted bool
|
||||
// ConvertedTime is a pointer to a time.Time object that indicates when the chapter was converted. Nil mean not converted.
|
||||
ConvertedTime time.Time
|
||||
}
|
||||
|
||||
// SetConverted sets the IsConverted field to true and sets the ConvertedTime field to the current time.
|
||||
func (chapter *Chapter) SetConverted() {
|
||||
chapter.IsConverted = true
|
||||
chapter.ConvertedTime = time.Now()
|
||||
}
|
18
internal/manga/page.go
Normal file
18
internal/manga/page.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package manga
|
||||
|
||||
import "bytes"
|
||||
|
||||
type Page struct {
|
||||
// Index of the page in the chapter.
|
||||
Index uint16 `json:"index" jsonschema:"description=Index of the page in the chapter."`
|
||||
// Extension of the page image.
|
||||
Extension string `json:"extension" jsonschema:"description=Extension of the page image."`
|
||||
// Size of the page in bytes
|
||||
Size uint64 `json:"-"`
|
||||
// Contents of the page
|
||||
Contents *bytes.Buffer `json:"-"`
|
||||
// IsSplitted tell us if the page was cropped to multiple pieces
|
||||
IsSplitted bool `json:"is_cropped" jsonschema:"description=Was this page cropped."`
|
||||
// SplitPartIndex represent the index of the crop if the page was cropped
|
||||
SplitPartIndex uint16 `json:"crop_part_index" jsonschema:"description=Index of the crop if the image was cropped."`
|
||||
}
|
19
internal/manga/page_container.go
Normal file
19
internal/manga/page_container.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package manga
|
||||
|
||||
import "image"
|
||||
|
||||
// PageContainer is a struct that holds a manga page, its image, and the image format.
|
||||
type PageContainer struct {
|
||||
// Page is a pointer to a manga page object.
|
||||
Page *Page
|
||||
// Image is the decoded image of the manga page.
|
||||
Image image.Image
|
||||
// Format is a string representing the format of the image (e.g., "png", "jpeg", "webp").
|
||||
Format string
|
||||
// IsToBeConverted is a boolean flag indicating whether the image needs to be converted to another format.
|
||||
IsToBeConverted bool
|
||||
}
|
||||
|
||||
func NewContainer(Page *Page, img image.Image, format string, isToBeConverted bool) *PageContainer {
|
||||
return &PageContainer{Page: Page, Image: img, Format: format, IsToBeConverted: isToBeConverted}
|
||||
}
|
25
internal/utils/errs/errors_defer.go
Normal file
25
internal/utils/errs/errors_defer.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package errs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Capture runs errFunc and assigns the error, if any, to *errPtr. Preserves the
|
||||
// original error by wrapping with errors.Join if the errFunc err is non-nil.
|
||||
func Capture(errPtr *error, errFunc func() error, msg string) {
|
||||
err := errFunc()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
*errPtr = errors.Join(*errPtr, fmt.Errorf("%s: %w", msg, err))
|
||||
}
|
||||
|
||||
// CaptureGeneric runs errFunc with a generic type K and assigns the error, if any, to *errPtr.
|
||||
func CaptureGeneric[K any](errPtr *error, errFunc func(value K) error, value K, msg string) {
|
||||
err := errFunc(value)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
*errPtr = errors.Join(*errPtr, fmt.Errorf("%s: %w", msg, err))
|
||||
}
|
122
internal/utils/errs/errors_defer_test.go
Normal file
122
internal/utils/errs/errors_defer_test.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package errs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCapture(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
initial error
|
||||
errFunc func() error
|
||||
msg string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "No error from errFunc",
|
||||
initial: nil,
|
||||
errFunc: func() error { return nil },
|
||||
msg: "test message",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "Error from errFunc with no initial error",
|
||||
initial: nil,
|
||||
errFunc: func() error { return errors.New("error from func") },
|
||||
msg: "test message",
|
||||
expected: "test message: error from func",
|
||||
},
|
||||
{
|
||||
name: "Error from errFunc with initial error",
|
||||
initial: errors.New("initial error"),
|
||||
errFunc: func() error { return errors.New("error from func") },
|
||||
msg: "test message",
|
||||
expected: "initial error\ntest message: error from func",
|
||||
},
|
||||
{
|
||||
name: "Error from errFunc with initial wrapped error",
|
||||
initial: fmt.Errorf("wrapped error: %w", errors.New("initial error")),
|
||||
errFunc: func() error { return errors.New("error from func") },
|
||||
msg: "test message",
|
||||
expected: "wrapped error: initial error\ntest message: error from func",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var err error = tt.initial
|
||||
Capture(&err, tt.errFunc, tt.msg)
|
||||
if err != nil && err.Error() != tt.expected {
|
||||
t.Errorf("expected %q, got %q", tt.expected, err.Error())
|
||||
} else if err == nil && tt.expected != "" {
|
||||
t.Errorf("expected %q, got nil", tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaptureGeneric(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
initial error
|
||||
errFunc func(int) error
|
||||
value int
|
||||
msg string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "No error from errFunc",
|
||||
initial: nil,
|
||||
errFunc: func(value int) error { return nil },
|
||||
value: 0,
|
||||
msg: "test message",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "Error from errFunc with no initial error",
|
||||
initial: nil,
|
||||
errFunc: func(value int) error { return errors.New("error from func") },
|
||||
value: 0,
|
||||
msg: "test message",
|
||||
expected: "test message: error from func",
|
||||
},
|
||||
{
|
||||
name: "Error from errFunc with initial error",
|
||||
initial: errors.New("initial error"),
|
||||
errFunc: func(value int) error { return errors.New("error from func") },
|
||||
value: 0,
|
||||
msg: "test message",
|
||||
expected: "initial error\ntest message: error from func",
|
||||
},
|
||||
{
|
||||
name: "Error from errFunc with initial wrapped error",
|
||||
initial: fmt.Errorf("wrapped error: %w", errors.New("initial error")),
|
||||
errFunc: func(value int) error { return errors.New("error from func") },
|
||||
value: 0,
|
||||
msg: "test message",
|
||||
expected: "wrapped error: initial error\ntest message: error from func",
|
||||
},
|
||||
{
|
||||
name: "Error from errFunc with initial wrapped error and value",
|
||||
initial: fmt.Errorf("wrapped error: %w", errors.New("initial error")),
|
||||
errFunc: func(value int) error { return fmt.Errorf("hello error:%d", value) },
|
||||
value: 1,
|
||||
msg: "test message",
|
||||
expected: "wrapped error: initial error\ntest message: hello error:1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var err error = tt.initial
|
||||
CaptureGeneric(&err, tt.errFunc, tt.value, tt.msg)
|
||||
if err != nil && err.Error() != tt.expected {
|
||||
t.Errorf("expected %q, got %q", tt.expected, err.Error())
|
||||
} else if err == nil && tt.expected != "" {
|
||||
t.Errorf("expected %q, got nil", tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
12
internal/utils/file_utils.go
Normal file
12
internal/utils/file_utils.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package utils
|
||||
|
||||
import "os"
|
||||
|
||||
// IsValidFolder checks if the provided path is a valid directory
|
||||
func IsValidFolder(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return info.IsDir()
|
||||
}
|
67
internal/utils/optimize.go
Normal file
67
internal/utils/optimize.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/belphemur/CBZOptimizer/v2/internal/cbz"
|
||||
"github.com/belphemur/CBZOptimizer/v2/pkg/converter"
|
||||
errors2 "github.com/belphemur/CBZOptimizer/v2/pkg/converter/errors"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type OptimizeOptions struct {
|
||||
ChapterConverter converter.Converter
|
||||
Path string
|
||||
Quality uint8
|
||||
Override bool
|
||||
Split bool
|
||||
}
|
||||
|
||||
// Optimize optimizes a CBZ file using the specified converter.
|
||||
func Optimize(options *OptimizeOptions) error {
|
||||
log.Printf("Processing file: %s\n", options.Path)
|
||||
|
||||
// Load the chapter
|
||||
chapter, err := cbz.LoadChapter(options.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load chapter: %v", err)
|
||||
}
|
||||
|
||||
if chapter.IsConverted {
|
||||
log.Printf("Chapter already converted: %s", options.Path)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert the chapter
|
||||
convertedChapter, err := options.ChapterConverter.ConvertChapter(chapter, options.Quality, options.Split, func(msg string, current uint32, total uint32) {
|
||||
if current%10 == 0 || current == total {
|
||||
log.Printf("[%s] Converting: %d/%d", chapter.FilePath, current, total)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
var pageIgnoredError *errors2.PageIgnoredError
|
||||
if !errors.As(err, &pageIgnoredError) {
|
||||
return fmt.Errorf("failed to convert chapter: %v", err)
|
||||
}
|
||||
}
|
||||
if convertedChapter == nil {
|
||||
return fmt.Errorf("failed to convert chapter")
|
||||
}
|
||||
|
||||
convertedChapter.SetConverted()
|
||||
|
||||
// Write the converted chapter back to a CBZ file
|
||||
outputPath := options.Path
|
||||
if !options.Override {
|
||||
outputPath = strings.TrimSuffix(options.Path, ".cbz") + "_converted.cbz"
|
||||
}
|
||||
err = cbz.WriteChapterToCBZ(convertedChapter, outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write converted chapter: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Converted file written to: %s\n", outputPath)
|
||||
return nil
|
||||
|
||||
}
|
Reference in New Issue
Block a user