mirror of
https://github.com/Belphemur/CBZOptimizer.git
synced 2026-01-11 16:17:04 +01:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba82003b53 | ||
|
|
5f7e7de644 | ||
|
|
5b183cca29 | ||
|
|
d901be14fa | ||
|
|
a80997835a | ||
|
|
37bb12fd61 | ||
|
|
c19afb9f40 | ||
|
|
911e1041ff | ||
|
|
a10d589b67 | ||
|
|
da508fcb3f | ||
|
|
57f5282032 | ||
|
|
d4f8d8b5ff | ||
|
|
1b026b9dbd | ||
|
|
12cc8d4e25 | ||
|
|
3442b2a845 | ||
|
|
b9a1fb213a | ||
|
|
278ee130e3 | ||
|
|
5357ece2b7 | ||
|
|
dbef43d376 | ||
|
|
7c63ea49c0 | ||
|
|
8a067939af | ||
|
|
f89974ac79 | ||
|
|
ce365a6bdf | ||
|
|
9e61ff4634 | ||
|
|
63a1b592c3 | ||
|
|
673484692b | ||
|
|
ad35e2655f | ||
|
|
d7f55fa886 | ||
|
|
62638517e4 | ||
|
|
dbf7f6c262 |
16
.github/workflows/test.yml
vendored
16
.github/workflows/test.yml
vendored
@@ -28,7 +28,21 @@ jobs:
|
||||
mv go-junit-report /usr/local/bin/
|
||||
|
||||
- name: Run tests
|
||||
run: go test -v 2>&1 ./... -coverprofile=coverage.txt | go-junit-report -set-exit-code > junit.xml
|
||||
run: |
|
||||
set -o pipefail
|
||||
go test -v 2>&1 ./... -coverprofile=coverage.txt | tee test-results.txt
|
||||
- name: Analyse test results
|
||||
if: ${{ !cancelled() }}
|
||||
run: go-junit-report < test-results.txt > junit.xml
|
||||
- name: Upload test result artifact
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-results
|
||||
path: |
|
||||
test-results.txt
|
||||
junit.xml
|
||||
retention-days: 7
|
||||
- name: Upload results to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
|
||||
@@ -3,13 +3,13 @@ LABEL authors="Belphemur"
|
||||
ENV USER=abc
|
||||
ENV CONFIG_FOLDER=/config
|
||||
ENV PUID=99
|
||||
RUN mkdir -p "${CONFIG_FOLDER}" && adduser \
|
||||
RUN adduser \
|
||||
--disabled-password \
|
||||
--gecos "" \
|
||||
--home "$(pwd)" \
|
||||
--ingroup "users" \
|
||||
--no-create-home \
|
||||
--uid "${PUID}" \
|
||||
--home "${CONFIG_FOLDER}" \
|
||||
"${USER}" && \
|
||||
chown ${PUID}:${GUID} "${CONFIG_FOLDER}"
|
||||
|
||||
@@ -17,5 +17,6 @@ COPY CBZOptimizer /usr/local/bin/CBZOptimizer
|
||||
|
||||
RUN apk add --no-cache inotify-tools bash-completion && chmod +x /usr/local/bin/CBZOptimizer && /usr/local/bin/CBZOptimizer completion bash > /etc/bash_completion.d/CBZOptimizer
|
||||
|
||||
VOLUME ${CONFIG_FOLDER}
|
||||
USER ${USER}
|
||||
ENTRYPOINT ["/usr/local/bin/CBZOptimizer"]
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"github.com/belphemur/CBZOptimizer/manga"
|
||||
"github.com/belphemur/CBZOptimizer/utils/errs"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
@@ -14,15 +15,14 @@ func WriteChapterToCBZ(chapter *manga.Chapter, outputFilePath string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create .cbz file: %w", err)
|
||||
}
|
||||
defer zipFile.Close()
|
||||
defer errs.Capture(&err, zipFile.Close, "failed to close .cbz file")
|
||||
|
||||
// Create a new ZIP writer
|
||||
zipWriter := zip.NewWriter(zipFile)
|
||||
err = zipWriter.SetComment("Created by CBZOptimizer")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer zipWriter.Close()
|
||||
defer errs.Capture(&err, zipWriter.Close, "failed to close .cbz writer")
|
||||
|
||||
// Write each page to the ZIP archive
|
||||
for _, page := range chapter.Pages {
|
||||
@@ -71,18 +71,11 @@ func WriteChapterToCBZ(chapter *manga.Chapter, outputFilePath string) error {
|
||||
}
|
||||
|
||||
if chapter.IsConverted {
|
||||
convertedWriter, err := zipWriter.CreateHeader(&zip.FileHeader{
|
||||
Name: "Converted.txt",
|
||||
Method: zip.Deflate,
|
||||
Modified: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create Converted.txt in .cbz: %w", err)
|
||||
}
|
||||
|
||||
_, err = convertedWriter.Write([]byte(fmt.Sprintf("%s\nThis chapter has been converted by CBZOptimizer.", chapter.ConvertedTime)))
|
||||
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 Converted.txt contents: %w", err)
|
||||
return fmt.Errorf("failed to write comment: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package cbz
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/belphemur/CBZOptimizer/manga"
|
||||
"os"
|
||||
"testing"
|
||||
@@ -10,11 +11,14 @@ import (
|
||||
)
|
||||
|
||||
func TestWriteChapterToCBZ(t *testing.T) {
|
||||
currentTime := time.Now()
|
||||
|
||||
// Define test cases
|
||||
testCases := []struct {
|
||||
name string
|
||||
chapter *manga.Chapter
|
||||
expectedFiles []string
|
||||
name string
|
||||
chapter *manga.Chapter
|
||||
expectedFiles []string
|
||||
expectedComment string
|
||||
}{
|
||||
//test case where there is only one page and ComicInfo and the chapter is converted
|
||||
{
|
||||
@@ -29,9 +33,10 @@ func TestWriteChapterToCBZ(t *testing.T) {
|
||||
},
|
||||
ComicInfoXml: "<Series>Boundless Necromancer</Series>",
|
||||
IsConverted: true,
|
||||
ConvertedTime: time.Now(),
|
||||
ConvertedTime: currentTime,
|
||||
},
|
||||
expectedFiles: []string{"0000.jpg", "ComicInfo.xml", "Converted.txt"},
|
||||
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
|
||||
{
|
||||
@@ -125,6 +130,10 @@ func TestWriteChapterToCBZ(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/araddon/dateparse"
|
||||
"github.com/belphemur/CBZOptimizer/manga"
|
||||
"github.com/belphemur/CBZOptimizer/utils/errs"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -18,72 +19,85 @@ func LoadChapter(filePath string) (*manga.Chapter, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open .cbz file: %w", err)
|
||||
}
|
||||
defer r.Close()
|
||||
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
|
||||
}
|
||||
// Open the file inside the zip
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open file inside .cbz: %w", err)
|
||||
}
|
||||
|
||||
// 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)
|
||||
err := func() error {
|
||||
// Open the file inside the zip
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
rc.Close()
|
||||
return nil, fmt.Errorf("failed to read ComicInfo.xml content: %w", err)
|
||||
return fmt.Errorf("failed to open file inside .cbz: %w", err)
|
||||
}
|
||||
chapter.ComicInfoXml = string(xmlContent)
|
||||
} else if ext == ".txt" && strings.ToLower(filepath.Base(f.Name)) == "converted.txt" {
|
||||
textContent, err := io.ReadAll(rc)
|
||||
if err != nil {
|
||||
rc.Close()
|
||||
return nil, 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)
|
||||
|
||||
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 {
|
||||
rc.Close()
|
||||
return nil, fmt.Errorf("failed to parse converted time: %w", err)
|
||||
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)
|
||||
}
|
||||
chapter.IsConverted = true
|
||||
}
|
||||
} else {
|
||||
// Read the file contents for page
|
||||
buf := new(bytes.Buffer)
|
||||
_, err = io.Copy(buf, rc)
|
||||
if err != nil {
|
||||
rc.Close()
|
||||
return nil, 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,
|
||||
}
|
||||
// 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)
|
||||
// Add the page to the chapter
|
||||
chapter.Pages = append(chapter.Pages, page)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rc.Close()
|
||||
|
||||
}
|
||||
|
||||
return chapter, nil
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestLoadChapter(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "Converted Chapter",
|
||||
filePath: "../testdata/Chapter 1_converted.cbz",
|
||||
filePath: "../testdata/Chapter 10_converted.cbz",
|
||||
expectedPages: 107,
|
||||
expectedSeries: "<Series>Boundless Necromancer</Series>",
|
||||
expectedConversion: true,
|
||||
|
||||
@@ -76,7 +76,7 @@ func WatchCommand(_ *cobra.Command, args []string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prepare converter: %v", err)
|
||||
}
|
||||
log.Printf("Watching [%s] with [override: %t, quality: %d, format: %s]", path, override, quality, converterType.String())
|
||||
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)
|
||||
@@ -116,7 +116,7 @@ func WatchCommand(_ *cobra.Command, args []string) error {
|
||||
case inotifywaitgo.CLOSE_WRITE, inotifywaitgo.MOVE:
|
||||
err := utils.Optimize(&utils.OptimizeOptions{
|
||||
ChapterConverter: chapterConverter,
|
||||
Path: path,
|
||||
Path: event.Filename,
|
||||
Quality: quality,
|
||||
Override: override,
|
||||
Split: split,
|
||||
|
||||
@@ -12,6 +12,9 @@ import (
|
||||
type Converter interface {
|
||||
// Format of the converter
|
||||
Format() (format constant.ConversionFormat)
|
||||
// ConvertChapter converts a manga chapter to the specified format.
|
||||
//
|
||||
// Returns partial success where some pages are converted and some are not.
|
||||
ConvertChapter(chapter *manga.Chapter, quality uint8, split bool, progress func(message string, current uint32, total uint32)) (*manga.Chapter, error)
|
||||
PrepareConverter() error
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package converter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/belphemur/CBZOptimizer/converter/constant"
|
||||
"github.com/belphemur/CBZOptimizer/manga"
|
||||
"golang.org/x/exp/slices"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"os"
|
||||
@@ -12,29 +14,46 @@ import (
|
||||
func TestConvertChapter(t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
genTestChapter func(path string) (*manga.Chapter, error)
|
||||
split bool
|
||||
name string
|
||||
genTestChapter func(path string) (*manga.Chapter, error)
|
||||
split bool
|
||||
expectFailure []constant.ConversionFormat
|
||||
expectPartialSuccess []constant.ConversionFormat
|
||||
}{
|
||||
{
|
||||
name: "All split pages",
|
||||
genTestChapter: genBigPages,
|
||||
split: true,
|
||||
name: "All split pages",
|
||||
genTestChapter: genHugePage,
|
||||
split: true,
|
||||
expectFailure: []constant.ConversionFormat{},
|
||||
expectPartialSuccess: []constant.ConversionFormat{},
|
||||
},
|
||||
{
|
||||
name: "Big Pages, no split",
|
||||
genTestChapter: genBigPages,
|
||||
split: false,
|
||||
name: "Big Pages, no split",
|
||||
genTestChapter: genHugePage,
|
||||
split: false,
|
||||
expectFailure: []constant.ConversionFormat{constant.WebP},
|
||||
expectPartialSuccess: []constant.ConversionFormat{},
|
||||
},
|
||||
{
|
||||
name: "No split pages",
|
||||
genTestChapter: genSmallPages,
|
||||
split: false,
|
||||
name: "No split pages",
|
||||
genTestChapter: genSmallPages,
|
||||
split: false,
|
||||
expectFailure: []constant.ConversionFormat{},
|
||||
expectPartialSuccess: []constant.ConversionFormat{},
|
||||
},
|
||||
{
|
||||
name: "Mix of split and no split pages",
|
||||
genTestChapter: genMixSmallBig,
|
||||
split: true,
|
||||
name: "Mix of split and no split pages",
|
||||
genTestChapter: genMixSmallBig,
|
||||
split: true,
|
||||
expectFailure: []constant.ConversionFormat{},
|
||||
expectPartialSuccess: []constant.ConversionFormat{},
|
||||
},
|
||||
{
|
||||
name: "Mix of Huge and small page",
|
||||
genTestChapter: genMixSmallHuge,
|
||||
split: false,
|
||||
expectFailure: []constant.ConversionFormat{},
|
||||
expectPartialSuccess: []constant.ConversionFormat{constant.WebP},
|
||||
},
|
||||
}
|
||||
// Load test genTestChapter from testdata
|
||||
@@ -63,15 +82,29 @@ func TestConvertChapter(t *testing.T) {
|
||||
t.Log(msg)
|
||||
}
|
||||
|
||||
convertedChapter, err := converter.ConvertChapter(chapter, quality, false, progress)
|
||||
convertedChapter, err := converter.ConvertChapter(chapter, quality, tc.split, progress)
|
||||
if err != nil {
|
||||
if convertedChapter != nil && slices.Contains(tc.expectPartialSuccess, converter.Format()) {
|
||||
t.Logf("Partial success to convert genTestChapter: %v", err)
|
||||
return
|
||||
}
|
||||
if slices.Contains(tc.expectFailure, converter.Format()) {
|
||||
t.Logf("Expected failure to convert genTestChapter: %v", err)
|
||||
return
|
||||
}
|
||||
t.Fatalf("failed to convert genTestChapter: %v", err)
|
||||
} else if slices.Contains(tc.expectFailure, converter.Format()) {
|
||||
t.Fatalf("expected failure to convert genTestChapter didn't happen")
|
||||
}
|
||||
|
||||
if len(convertedChapter.Pages) == 0 {
|
||||
t.Fatalf("no pages were converted")
|
||||
}
|
||||
|
||||
if len(convertedChapter.Pages) != len(chapter.Pages) {
|
||||
t.Fatalf("converted chapter has different number of pages")
|
||||
}
|
||||
|
||||
for _, page := range convertedChapter.Pages {
|
||||
if page.Extension != ".webp" {
|
||||
t.Errorf("page %d was not converted to webp format", page.Index)
|
||||
@@ -83,7 +116,7 @@ func TestConvertChapter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func genBigPages(path string) (*manga.Chapter, error) {
|
||||
func genHugePage(path string) (*manga.Chapter, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -91,8 +124,8 @@ func genBigPages(path string) (*manga.Chapter, error) {
|
||||
defer file.Close()
|
||||
|
||||
var pages []*manga.Page
|
||||
for i := 0; i < 5; i++ { // Assuming there are 5 pages for the test
|
||||
img := image.NewRGBA(image.Rect(0, 0, 300, 17000))
|
||||
for i := 0; i < 1; i++ { // Assuming there are 5 pages for the test
|
||||
img := image.NewRGBA(image.Rect(0, 0, 1, 17000))
|
||||
buf := new(bytes.Buffer)
|
||||
err := jpeg.Encode(buf, img, nil)
|
||||
if err != nil {
|
||||
@@ -169,3 +202,32 @@ func genMixSmallBig(path string) (*manga.Chapter, error) {
|
||||
Pages: pages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func genMixSmallHuge(path string) (*manga.Chapter, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var pages []*manga.Page
|
||||
for i := 0; i < 10; i++ { // Assuming there are 5 pages for the test
|
||||
img := image.NewRGBA(image.Rect(0, 0, 1, 2000*(i+1)))
|
||||
buf := new(bytes.Buffer)
|
||||
err := jpeg.Encode(buf, img, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
page := &manga.Page{
|
||||
Index: uint16(i),
|
||||
Contents: buf,
|
||||
Extension: ".jpg",
|
||||
}
|
||||
pages = append(pages, page)
|
||||
}
|
||||
|
||||
return &manga.Chapter{
|
||||
FilePath: path,
|
||||
Pages: pages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
13
converter/errors/converter_errors.go
Normal file
13
converter/errors/converter_errors.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package errors
|
||||
|
||||
type PageIgnoredError struct {
|
||||
s string
|
||||
}
|
||||
|
||||
func (e *PageIgnoredError) Error() string {
|
||||
return e.s
|
||||
}
|
||||
|
||||
func NewPageIgnored(text string) error {
|
||||
return &PageIgnoredError{text}
|
||||
}
|
||||
@@ -2,8 +2,10 @@ package webp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/belphemur/CBZOptimizer/converter/constant"
|
||||
converterrors "github.com/belphemur/CBZOptimizer/converter/errors"
|
||||
"github.com/belphemur/CBZOptimizer/manga"
|
||||
"github.com/oliamb/cutter"
|
||||
"golang.org/x/exp/slices"
|
||||
@@ -106,22 +108,25 @@ func (converter *Converter) ConvertChapter(chapter *manga.Chapter, quality uint8
|
||||
go func(page *manga.Page) {
|
||||
defer wgPages.Done()
|
||||
|
||||
splitNeeded, img, format, err := converter.checkPageNeedsSplit(page)
|
||||
// Respect choice to split or not
|
||||
splitNeeded = split && splitNeeded
|
||||
splitNeeded, img, format, err := converter.checkPageNeedsSplit(page, split)
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("error checking if page %d of genTestChapter %s needs split: %v", page.Index, chapter.FilePath, err)
|
||||
errChan <- err
|
||||
// Partial error in this case, we want the page, but not converting it
|
||||
if img != nil {
|
||||
wgConvertedPages.Add(1)
|
||||
pagesChan <- manga.NewContainer(page, img, format, false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !splitNeeded {
|
||||
wgConvertedPages.Add(1)
|
||||
pagesChan <- manga.NewContainer(page, img, format)
|
||||
pagesChan <- manga.NewContainer(page, img, format, true)
|
||||
return
|
||||
}
|
||||
images, err := converter.cropImage(img)
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("error converting page %d of genTestChapter %s to webp: %v", page.Index, chapter.FilePath, err)
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
@@ -129,7 +134,7 @@ func (converter *Converter) ConvertChapter(chapter *manga.Chapter, quality uint8
|
||||
for i, img := range images {
|
||||
page := &manga.Page{Index: page.Index, IsSplitted: true, SplitPartIndex: uint16(i)}
|
||||
wgConvertedPages.Add(1)
|
||||
pagesChan <- manga.NewContainer(page, img, "N/A")
|
||||
pagesChan <- manga.NewContainer(page, img, "N/A", true)
|
||||
}
|
||||
}(page)
|
||||
}
|
||||
@@ -144,8 +149,9 @@ func (converter *Converter) ConvertChapter(chapter *manga.Chapter, quality uint8
|
||||
errList = append(errList, err)
|
||||
}
|
||||
|
||||
var aggregatedError error = nil
|
||||
if len(errList) > 0 {
|
||||
return nil, fmt.Errorf("encountered errors: %v", errList)
|
||||
aggregatedError = errors.Join(errList...)
|
||||
}
|
||||
|
||||
slices.SortFunc(pages, func(a, b *manga.Page) int {
|
||||
@@ -158,7 +164,7 @@ func (converter *Converter) ConvertChapter(chapter *manga.Chapter, quality uint8
|
||||
|
||||
runtime.GC()
|
||||
|
||||
return chapter, nil
|
||||
return chapter, aggregatedError
|
||||
}
|
||||
|
||||
func (converter *Converter) cropImage(img image.Image) ([]image.Image, error) {
|
||||
@@ -194,7 +200,7 @@ func (converter *Converter) cropImage(img image.Image) ([]image.Image, error) {
|
||||
return parts, nil
|
||||
}
|
||||
|
||||
func (converter *Converter) checkPageNeedsSplit(page *manga.Page) (bool, image.Image, string, error) {
|
||||
func (converter *Converter) checkPageNeedsSplit(page *manga.Page, splitRequested bool) (bool, image.Image, string, error) {
|
||||
reader := io.Reader(bytes.NewBuffer(page.Contents.Bytes()))
|
||||
img, format, err := image.Decode(reader)
|
||||
if err != nil {
|
||||
@@ -204,16 +210,19 @@ func (converter *Converter) checkPageNeedsSplit(page *manga.Page) (bool, image.I
|
||||
bounds := img.Bounds()
|
||||
height := bounds.Dy()
|
||||
|
||||
if height >= webpMaxHeight {
|
||||
return false, img, format, fmt.Errorf("page[%d] height %d exceeds maximum height %d of webp format", page.Index, height, webpMaxHeight)
|
||||
if height >= webpMaxHeight && !splitRequested {
|
||||
return false, img, format, converterrors.NewPageIgnored(fmt.Sprintf("page %d is too tall [max: %dpx] to be converted to webp format", page.Index, webpMaxHeight))
|
||||
}
|
||||
return height >= converter.maxHeight, img, format, nil
|
||||
return height >= converter.maxHeight && splitRequested, img, format, nil
|
||||
}
|
||||
|
||||
func (converter *Converter) convertPage(container *manga.PageContainer, quality uint8) (*manga.PageContainer, error) {
|
||||
if container.Format == "webp" {
|
||||
return container, nil
|
||||
}
|
||||
if !container.IsToBeConverted {
|
||||
return container, nil
|
||||
}
|
||||
converted, err := converter.convert(container.Image, uint(quality))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
6
go.mod
6
go.mod
@@ -11,8 +11,8 @@ require (
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/viper v1.19.0
|
||||
github.com/thediveo/enumflag/v2 v2.0.5
|
||||
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948
|
||||
golang.org/x/image v0.19.0
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0
|
||||
golang.org/x/image v0.20.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -43,7 +43,7 @@ require (
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
16
go.sum
16
go.sum
@@ -118,18 +118,18 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=
|
||||
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ=
|
||||
golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
||||
golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw=
|
||||
golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM=
|
||||
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
|
||||
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
|
||||
@@ -10,8 +10,10 @@ type PageContainer struct {
|
||||
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) *PageContainer {
|
||||
return &PageContainer{Page: Page, Image: img, Format: format}
|
||||
func NewContainer(Page *Page, img image.Image, format string, isToBeConverted bool) *PageContainer {
|
||||
return &PageContainer{Page: Page, Image: img, Format: format, IsToBeConverted: isToBeConverted}
|
||||
}
|
||||
|
||||
@@ -2,5 +2,16 @@
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchUpdateTypes": [
|
||||
"minor",
|
||||
"patch",
|
||||
"digest"
|
||||
],
|
||||
"matchCurrentVersion": "!/^0/",
|
||||
"automerge": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
16
utils/errs/errors_defer.go
Normal file
16
utils/errs/errors_defer.go
Normal file
@@ -0,0 +1,16 @@
|
||||
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))
|
||||
}
|
||||
58
utils/errs/errors_defer_test.go
Normal file
58
utils/errs/errors_defer_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/belphemur/CBZOptimizer/cbz"
|
||||
"github.com/belphemur/CBZOptimizer/converter"
|
||||
errors2 "github.com/belphemur/CBZOptimizer/converter/errors"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
@@ -38,8 +40,15 @@ func Optimize(options *OptimizeOptions) error {
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert chapter: %v", err)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user