mirror of
https://github.com/Belphemur/CBZOptimizer.git
synced 2026-01-11 16:17:04 +01:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c63ea49c0 | ||
|
|
8a067939af | ||
|
|
f89974ac79 | ||
|
|
ce365a6bdf | ||
|
|
9e61ff4634 | ||
|
|
63a1b592c3 | ||
|
|
673484692b | ||
|
|
ad35e2655f | ||
|
|
d7f55fa886 | ||
|
|
62638517e4 | ||
|
|
dbf7f6c262 |
12
.github/workflows/test.yml
vendored
12
.github/workflows/test.yml
vendored
@@ -28,7 +28,17 @@ jobs:
|
|||||||
mv go-junit-report /usr/local/bin/
|
mv go-junit-report /usr/local/bin/
|
||||||
|
|
||||||
- name: Run tests
|
- 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
|
||||||
|
run: go-junit-report < test-results.txt > report.xml
|
||||||
|
- name: Upload test result artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: test-results
|
||||||
|
path: test-results.txt
|
||||||
|
retention-days: 7
|
||||||
- name: Upload results to Codecov
|
- name: Upload results to Codecov
|
||||||
uses: codecov/codecov-action@v4
|
uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ LABEL authors="Belphemur"
|
|||||||
ENV USER=abc
|
ENV USER=abc
|
||||||
ENV CONFIG_FOLDER=/config
|
ENV CONFIG_FOLDER=/config
|
||||||
ENV PUID=99
|
ENV PUID=99
|
||||||
RUN mkdir -p "${CONFIG_FOLDER}" && adduser \
|
RUN adduser \
|
||||||
--disabled-password \
|
--disabled-password \
|
||||||
--gecos "" \
|
--gecos "" \
|
||||||
--home "$(pwd)" \
|
--home "$(pwd)" \
|
||||||
--ingroup "users" \
|
--ingroup "users" \
|
||||||
--no-create-home \
|
|
||||||
--uid "${PUID}" \
|
--uid "${PUID}" \
|
||||||
|
--home "${CONFIG_FOLDER}" \
|
||||||
"${USER}" && \
|
"${USER}" && \
|
||||||
chown ${PUID}:${GUID} "${CONFIG_FOLDER}"
|
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
|
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}
|
USER ${USER}
|
||||||
ENTRYPOINT ["/usr/local/bin/CBZOptimizer"]
|
ENTRYPOINT ["/usr/local/bin/CBZOptimizer"]
|
||||||
@@ -12,6 +12,9 @@ import (
|
|||||||
type Converter interface {
|
type Converter interface {
|
||||||
// Format of the converter
|
// Format of the converter
|
||||||
Format() (format constant.ConversionFormat)
|
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)
|
ConvertChapter(chapter *manga.Chapter, quality uint8, split bool, progress func(message string, current uint32, total uint32)) (*manga.Chapter, error)
|
||||||
PrepareConverter() error
|
PrepareConverter() error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package converter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"github.com/belphemur/CBZOptimizer/converter/constant"
|
||||||
"github.com/belphemur/CBZOptimizer/manga"
|
"github.com/belphemur/CBZOptimizer/manga"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
"image"
|
"image"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
"os"
|
"os"
|
||||||
@@ -12,29 +14,46 @@ import (
|
|||||||
func TestConvertChapter(t *testing.T) {
|
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
|
split bool
|
||||||
|
expectFailure []constant.ConversionFormat
|
||||||
|
expectPartialSuccess []constant.ConversionFormat
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "All split pages",
|
name: "All split pages",
|
||||||
genTestChapter: genBigPages,
|
genTestChapter: genHugePage,
|
||||||
split: true,
|
split: true,
|
||||||
|
expectFailure: []constant.ConversionFormat{},
|
||||||
|
expectPartialSuccess: []constant.ConversionFormat{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Big Pages, no split",
|
name: "Big Pages, no split",
|
||||||
genTestChapter: genBigPages,
|
genTestChapter: genHugePage,
|
||||||
split: false,
|
split: false,
|
||||||
|
expectFailure: []constant.ConversionFormat{constant.WebP},
|
||||||
|
expectPartialSuccess: []constant.ConversionFormat{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "No split pages",
|
name: "No split pages",
|
||||||
genTestChapter: genSmallPages,
|
genTestChapter: genSmallPages,
|
||||||
split: false,
|
split: false,
|
||||||
|
expectFailure: []constant.ConversionFormat{},
|
||||||
|
expectPartialSuccess: []constant.ConversionFormat{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Mix of split and no split pages",
|
name: "Mix of split and no split pages",
|
||||||
genTestChapter: genMixSmallBig,
|
genTestChapter: genMixSmallBig,
|
||||||
split: true,
|
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
|
// Load test genTestChapter from testdata
|
||||||
@@ -63,15 +82,29 @@ func TestConvertChapter(t *testing.T) {
|
|||||||
t.Log(msg)
|
t.Log(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
convertedChapter, err := converter.ConvertChapter(chapter, quality, false, progress)
|
convertedChapter, err := converter.ConvertChapter(chapter, quality, tc.split, progress)
|
||||||
if err != nil {
|
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)
|
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 {
|
if len(convertedChapter.Pages) == 0 {
|
||||||
t.Fatalf("no pages were converted")
|
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 {
|
for _, page := range convertedChapter.Pages {
|
||||||
if page.Extension != ".webp" {
|
if page.Extension != ".webp" {
|
||||||
t.Errorf("page %d was not converted to webp format", page.Index)
|
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)
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -91,8 +124,8 @@ func genBigPages(path string) (*manga.Chapter, error) {
|
|||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
var pages []*manga.Page
|
var pages []*manga.Page
|
||||||
for i := 0; i < 5; i++ { // Assuming there are 5 pages for the test
|
for i := 0; i < 1; i++ { // Assuming there are 5 pages for the test
|
||||||
img := image.NewRGBA(image.Rect(0, 0, 300, 17000))
|
img := image.NewRGBA(image.Rect(0, 0, 1, 17000))
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
err := jpeg.Encode(buf, img, nil)
|
err := jpeg.Encode(buf, img, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -169,3 +202,32 @@ func genMixSmallBig(path string) (*manga.Chapter, error) {
|
|||||||
Pages: pages,
|
Pages: pages,
|
||||||
}, nil
|
}, 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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/belphemur/CBZOptimizer/converter/constant"
|
"github.com/belphemur/CBZOptimizer/converter/constant"
|
||||||
|
converterrors "github.com/belphemur/CBZOptimizer/converter/errors"
|
||||||
"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"
|
||||||
@@ -106,22 +108,25 @@ func (converter *Converter) ConvertChapter(chapter *manga.Chapter, quality uint8
|
|||||||
go func(page *manga.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, split)
|
||||||
// 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 <- 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !splitNeeded {
|
if !splitNeeded {
|
||||||
wgConvertedPages.Add(1)
|
wgConvertedPages.Add(1)
|
||||||
pagesChan <- manga.NewContainer(page, img, format)
|
pagesChan <- manga.NewContainer(page, img, format, true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
images, err := converter.cropImage(img)
|
images, err := converter.cropImage(img)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errChan <- fmt.Errorf("error converting page %d of genTestChapter %s to webp: %v", page.Index, chapter.FilePath, err)
|
errChan <- err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +134,7 @@ func (converter *Converter) ConvertChapter(chapter *manga.Chapter, quality uint8
|
|||||||
for i, img := range images {
|
for i, img := range images {
|
||||||
page := &manga.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 <- manga.NewContainer(page, img, "N/A")
|
pagesChan <- manga.NewContainer(page, img, "N/A", true)
|
||||||
}
|
}
|
||||||
}(page)
|
}(page)
|
||||||
}
|
}
|
||||||
@@ -144,8 +149,9 @@ func (converter *Converter) ConvertChapter(chapter *manga.Chapter, quality uint8
|
|||||||
errList = append(errList, err)
|
errList = append(errList, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var aggregatedError error = nil
|
||||||
if len(errList) > 0 {
|
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 {
|
slices.SortFunc(pages, func(a, b *manga.Page) int {
|
||||||
@@ -158,7 +164,7 @@ func (converter *Converter) ConvertChapter(chapter *manga.Chapter, quality uint8
|
|||||||
|
|
||||||
runtime.GC()
|
runtime.GC()
|
||||||
|
|
||||||
return chapter, nil
|
return chapter, aggregatedError
|
||||||
}
|
}
|
||||||
|
|
||||||
func (converter *Converter) cropImage(img image.Image) ([]image.Image, error) {
|
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
|
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()))
|
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 {
|
||||||
@@ -204,16 +210,19 @@ func (converter *Converter) checkPageNeedsSplit(page *manga.Page) (bool, image.I
|
|||||||
bounds := img.Bounds()
|
bounds := img.Bounds()
|
||||||
height := bounds.Dy()
|
height := bounds.Dy()
|
||||||
|
|
||||||
if height >= webpMaxHeight {
|
if height >= webpMaxHeight && !splitRequested {
|
||||||
return false, img, format, fmt.Errorf("page[%d] height %d exceeds maximum height %d of webp format", page.Index, height, webpMaxHeight)
|
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) {
|
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
|
||||||
}
|
}
|
||||||
|
if !container.IsToBeConverted {
|
||||||
|
return container, nil
|
||||||
|
}
|
||||||
converted, err := converter.convert(container.Image, uint(quality))
|
converted, err := converter.convert(container.Image, uint(quality))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ type PageContainer struct {
|
|||||||
Image image.Image
|
Image image.Image
|
||||||
// Format is a string representing the format of the image (e.g., "png", "jpeg", "webp").
|
// Format is a string representing the format of the image (e.g., "png", "jpeg", "webp").
|
||||||
Format string
|
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 {
|
func NewContainer(Page *Page, img image.Image, format string, isToBeConverted bool) *PageContainer {
|
||||||
return &PageContainer{Page: Page, Image: img, Format: format}
|
return &PageContainer{Page: Page, Image: img, Format: format, IsToBeConverted: isToBeConverted}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/belphemur/CBZOptimizer/cbz"
|
"github.com/belphemur/CBZOptimizer/cbz"
|
||||||
"github.com/belphemur/CBZOptimizer/converter"
|
"github.com/belphemur/CBZOptimizer/converter"
|
||||||
|
errors2 "github.com/belphemur/CBZOptimizer/converter/errors"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -38,8 +40,15 @@ func Optimize(options *OptimizeOptions) error {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err != nil {
|
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()
|
convertedChapter.SetConverted()
|
||||||
|
|
||||||
// Write the converted chapter back to a CBZ file
|
// Write the converted chapter back to a CBZ file
|
||||||
|
|||||||
Reference in New Issue
Block a user