From 1c6dfc880d6f3d52f012b3230baf86b6d3606c00 Mon Sep 17 00:00:00 2001 From: bakonpancakz Date: Sat, 23 May 2026 17:16:14 -0700 Subject: [PATCH] Initial Release --- .gitignore | 4 + LICENSE | 21 ++ README.md | 93 ++++++ crunchy/example.png | Bin 0 -> 57731 bytes crunchy/go.mod | 5 + crunchy/go.sum | 2 + crunchy/main.go | 193 ++++++++++++ imageconvert/main.go | 208 +++++++++++++ mangapub/go.mod | 5 + mangapub/go.sum | 2 + mangapub/main.go | 484 +++++++++++++++++++++++++++++++ mangapub/templates/container.xml | 6 + mangapub/templates/content.opf | 24 ++ mangapub/templates/page.xml | 16 + mangapub/templates/toc.ncx | 23 ++ mediaconvert/main.go | 329 +++++++++++++++++++++ 16 files changed, 1415 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 crunchy/example.png create mode 100644 crunchy/go.mod create mode 100644 crunchy/go.sum create mode 100644 crunchy/main.go create mode 100644 imageconvert/main.go create mode 100644 mangapub/go.mod create mode 100644 mangapub/go.sum create mode 100644 mangapub/main.go create mode 100644 mangapub/templates/container.xml create mode 100644 mangapub/templates/content.opf create mode 100644 mangapub/templates/page.xml create mode 100644 mangapub/templates/toc.ncx create mode 100644 mediaconvert/main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dfd6d15 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.vestige +dependencies +bin +lib \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8762813 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 bakonpancakz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7da3a35 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ + +# `>_` clitools + +A collection of simple CLI tools which I use ocasionally. +They're really simple, mostly single-file, and should theoretically "just work" across platforms. + +- [`(imageconvert)` Mass Image Converter using ImageMagick](#imageconvert-mass-image-converter-using-imagemagick) +- [`(mediaconvert)` Mass Media Converter using FFMPEG](#mediaconvert-mass-media-converter-using-ffmpeg) +- [`(crunchy)` Turn Image into a Crunchy JPEG](#crunchy-turn-image-into-a-crunchy-jpeg) +- [`(mangapub)` Converts CBZs into EPUBs](#mangapub-converts-cbzs-into-epubs) + +
+ +## `(imageconvert)` Mass Image Converter using ImageMagick +Ever have a directory full of images in different formats? Use this tool to +quickly convert them to a normal extension. Outputs to a `convert` folder +in the current working directory. + +> **Requires:** ImageMagick + +``` +imageconvert + --skip-errors - Skip on conversion error + --skip-resume - Skip Resume Checking + --multithread - Use Multiple Threads + --recursive - Scan Directories Recursively + - File Extension(s) to convert from, delimited with comma + - File Extension to convert into + [Arguments] - Arguments to pass onto ImageMagick +``` + +
+ +## `(mediaconvert)` Mass Media Converter using FFMPEG +Ever have a directory full of videos in different formats? Use this tool to +quickly convert them to a normal extension. Outputs to a `convert` folder +in the current working directory. + +> **Requires:** FFMPEG + +``` +mediaconvert + --skip-resume - Skip Resume Checking + --multithread - Use Multiple Threads + --recursive - Scan Directories Recursively + - File Extension(s) to convert from, delimited with comma + - File Extension to convert into + [Arguments] - Arguments to pass onto FFMPEG +Templates: + {filename} - Full Filename (e.g. myfile.txt) + {basename} - Base Filename (e.g. myfile + {directory} - Source Directory (e.g. /path/to/file) +``` + +
+ +## `(crunchy)` Turn Image into a Crunchy JPEG +Applies random noise and rounding errors to the colorspace to make an image look **"crunchy"** + +``` +crunchy + --noise= - Noise Level (Default: 25, Range: 0-100) + --quality= - JPEG Quality (Default: 0, Range: 0-100) + --generations= - Iterations (Default: 5) + - Input Filename +``` + +

+ +

Another satisfied customer!

+

+ +
+ +## `(mangapub)` Converts CBZs into EPUBs +Converts a directory of CBZ files into EPUBs, designed for copying mass amounts +of manga onto a Kindle 8th gen. It's default settings are very crunchy! + +**Note:** This doesn't properly split large images into two, and it will never, +because it doesn't bother me :P + +``` +mangapub + --extract - Extract Images to Directory + --recursive - Scan Directories Recursively + --height= - Image Height (Default: 800) + --width= - Image Width (Default: 600) + --quality= - JPEG Quality (Default: 25, Range: 0-100) +``` + +> Highly modified version of this repo: https://github.com/DimazzzZ/cbz2epub + +
diff --git a/crunchy/example.png b/crunchy/example.png new file mode 100644 index 0000000000000000000000000000000000000000..fe135b63c86b4b43e656558b50a28f00035ca227 GIT binary patch literal 57731 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 literal 0 HcmV?d00001 diff --git a/crunchy/go.mod b/crunchy/go.mod new file mode 100644 index 0000000..a917958 --- /dev/null +++ b/crunchy/go.mod @@ -0,0 +1,5 @@ +module github.com/bakonpancakz/clitools/crunchy + +go 1.25.2 + +require golang.org/x/image v0.33.0 diff --git a/crunchy/go.sum b/crunchy/go.sum new file mode 100644 index 0000000..ba0ff29 --- /dev/null +++ b/crunchy/go.sum @@ -0,0 +1,2 @@ +golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ= +golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc= diff --git a/crunchy/main.go b/crunchy/main.go new file mode 100644 index 0000000..6dda63f --- /dev/null +++ b/crunchy/main.go @@ -0,0 +1,193 @@ +package main + +import ( + "bytes" + "errors" + "fmt" + "image" + "image/color" + "image/gif" + "image/jpeg" + "image/png" + "io" + "math" + "math/rand" + "os" + "path" + "strconv" + "strings" + + "golang.org/x/image/webp" +) + +func main() { + + // ----- Parse Arguments ----- + var optionQuality = 0 + var optionNoise = 25 + var optionGenerations = 5 + var optionFilename string + + flags := make([]string, 0, len(os.Args)) + for i := 1; i < len(os.Args); i++ { + segments := strings.SplitN(os.Args[i], "=", 2) + if len(segments) == 2 { + n := segments[0] + s := segments[1] + switch { + case strings.EqualFold(n, "--generations"): + v := parseInteger(n, s, 0, math.MaxInt) + fmt.Printf("Flag: Generation(s) %d\n", v) + optionGenerations = v + + case strings.EqualFold(n, "--quality"): + v := parseInteger(n, s, 0, 100) + fmt.Printf("Flag: Quality %d\n", v) + optionQuality = v + + case strings.EqualFold(n, "--noise"): + v := parseInteger(n, s, 0, 100) + fmt.Printf("Flag: Noise Level %d\n", v) + optionQuality = v + + default: + fmt.Printf("%s: Unknown Argument", n) + os.Exit(1) + } + + } else { + flags = append(flags, segments[0]) + } + } + if len(flags) < 1 { + fmt.Println("crunchy") + fmt.Println(" --noise= - Noise Level (Default: 25, Range: 0-100)") + fmt.Println(" --quality= - JPEG Quality (Default: 0, Range: 0-100)") + fmt.Println(" --generations= - Iterations (Default: 5)") + fmt.Println(" - Input Filename") + os.Exit(0) + } + optionFilename = flags[0] + noiseInteger := int(float32(optionNoise)*2.56) + 1 + noiseHalved := noiseInteger / 2 + + // ----- Decode Image Contents ----- + content := bytes.Buffer{} + f, err := os.Open(optionFilename) + if err != nil { + fmt.Printf("Failed to open file: %s\n", err.Error()) + os.Exit(1) + } + if _, err := io.Copy(&content, f); err != nil { + fmt.Printf("Failed to read file: %s\n", err.Error()) + os.Exit(1) + } + + img, err := decodeImage(content.Bytes()) + if err != nil { + fmt.Printf("Decoding Error: %s\n", err.Error()) + os.Exit(1) + } + bounds := img.Bounds() + ycc := image.NewYCbCr(bounds, image.YCbCrSubsampleRatio444) + rgb := image.NewRGBA(bounds) + for y := 0; y < bounds.Dy(); y++ { + for x := 0; x < bounds.Dx(); x++ { + rgb.Set(x, y, img.At(x, y)) // copy generic img to rgba + } + } + + // ----- Apply Generation Loss ----- + for i := 0; i < optionGenerations; i++ { + for y := 0; y < bounds.Dy(); y++ { + for x := 0; x < bounds.Dx(); x++ { + // Rounding Error via Colorspace Conversion + r, g, b, _ := rgb.At(x, y).RGBA() + cy, cb, cr := color.RGBToYCbCr(uint8(r>>8), uint8(g>>8), uint8(b>>8)) + + // Random Noise + noise := rand.Intn(noiseInteger) - noiseHalved + cb = uint8(int(cb) + noise) + cr = uint8(int(cr) + noise) + + // Apply Changes + ycc.Y[ycc.YOffset(x, y)] = cy + ycc.Cb[ycc.COffset(x, y)] = cb + ycc.Cr[ycc.COffset(x, y)] = cr + } + } + for y := 0; y < bounds.Dy(); y++ { + for x := 0; x < bounds.Dx(); x++ { + rgb.Set(x, y, ycc.At(x, y)) + } + } + } + + // ----- Write Output ----- + content.Reset() + if err := jpeg.Encode(&content, rgb, &jpeg.Options{Quality: optionQuality}); err != nil { + fmt.Printf("Encoding Error: %s\n", err.Error()) + os.Exit(1) + } + cleanname := path.Base(optionFilename) + emptyname := strings.TrimSuffix(cleanname, path.Ext(cleanname)) + finalname := fmt.Sprintf("%s_n%d_g%d_q%d.jpeg", emptyname, optionNoise, optionGenerations, optionQuality) + if err := os.WriteFile(finalname, content.Bytes(), 0660); err != nil { + fmt.Printf("Failed to write file '%s': %s\n", finalname, err.Error()) + os.Exit(1) + } +} + +// Parse Integer for CLI Arguments +func parseInteger(n string, s string, min int, max int) int { + v, err := strconv.Atoi(s) + if err != nil { + fmt.Printf("%s: Not A Number\n", n) + os.Exit(1) + } + if v < min { + fmt.Printf("%s: Value cannot be less than %d\n", n, min) + os.Exit(1) + } + if v > max { + fmt.Printf("%s: Value cannot be more than %d\n", n, max) + os.Exit(1) + } + return v +} + +// Decode Image with the appropriate decoder based on it's starting bytes +// https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files) +func decodeImage(d []byte) (image.Image, error) { + var ( + decoderImage image.Image + decoderError error + ) + switch { + case len(d) > 3 && // JPEG + d[0] == 0xFF && d[1] == 0xD8 && d[2] == 0xFF: + decoderImage, decoderError = jpeg.Decode(bytes.NewReader(d)) + + case len(d) > 8 && // PNG + d[0] == 0x89 && d[1] == 0x50 && d[2] == 0x4E && d[3] == 0x47 && + d[4] == 0x0D && d[5] == 0x0A && d[6] == 0x1A && d[7] == 0x0A: + decoderImage, decoderError = png.Decode(bytes.NewReader(d)) + + case len(d) > 4 && // GIF + d[0] == 0x47 && d[1] == 0x49 && d[2] == 0x46 && d[3] == 0x38: + decoderImage, decoderError = gif.Decode(bytes.NewReader(d)) + + case len(d) > 12 && // WEBP + d[0] == 0x52 && d[1] == 0x49 && d[2] == 0x46 && d[3] == 0x46 && + d[8] == 0x57 && d[9] == 0x45 && d[10] == 0x42 && d[11] == 0x50: + decoderImage, decoderError = webp.Decode(bytes.NewReader(d)) + + default: + return decoderImage, errors.New("unsupported file type") + } + if decoderError != nil { + return nil, decoderError + } + + return decoderImage, nil +} diff --git a/imageconvert/main.go b/imageconvert/main.go new file mode 100644 index 0000000..6624454 --- /dev/null +++ b/imageconvert/main.go @@ -0,0 +1,208 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/exec" + "path" + "runtime" + "strings" + "sync" + "sync/atomic" + "time" +) + +type QueuedItem struct { + Basename string // Filename without Extension + Filename string // Filename with Extension + Nest []string // Subdirectories +} + +const ( + OUTPUT_DIR = "convert" + OUTPUT_FLAG = 0755 +) + +var ( + featureResume bool = true + featureRecursive bool = false + featureSkipErrors bool = false + flags []string + queue []QueuedItem + workers = 1 +) + +func scan(nest []string, extensions []string) { + if len(nest) == 1 && strings.EqualFold(nest[0], OUTPUT_DIR) { + return + } + folder := path.Join(nest...) + if folder == "" { + folder = "." + } + files, err := os.ReadDir(folder) + if err != nil { + log.Fatalf("Error reading directory '%s': %s\n", folder, err) + } + for _, entry := range files { + filename := entry.Name() + + // Scan Subdirectory + if entry.IsDir() { + if featureRecursive { + scan(append(nest, filename), extensions) + } + continue + } + + // Match File Extension + for _, prefix := range extensions { + if len(filename) < len(prefix) { + continue + } + if !strings.EqualFold(filename[len(filename)-len(prefix):], prefix) { + continue + } + + // Perform Resume Check + basename := filename[:strings.LastIndex(filename, ".")] + location := fmt.Sprint(path.Join(OUTPUT_DIR, folder, basename), ".", flags[2]) + if featureResume { + if info, err := os.Stat(location); err == nil { + if info.Size() != 0 { + fmt.Printf("Skipping '%s' as it is already complete\n", filename) + continue + } + } + } + + // Add Item to Queue + queue = append(queue, QueuedItem{ + Filename: filename, + Basename: basename, + Nest: nest, + }) + break + } + } +} + +func main() { + t := time.Now() + + // Collect Arguments + for _, arg := range os.Args { + if strings.EqualFold(arg, "--skip-resume") { + log.Println("Flag: Disabling Resume Check") + featureResume = false + continue + } + if strings.EqualFold(arg, "--recursive") { + log.Println("Flag: Scanning Recursively") + featureRecursive = true + continue + } + if strings.EqualFold(arg, "--multithread") { + log.Println("Flag: Enabling Multi-threading") + workers = runtime.NumCPU() - 1 + if workers < 1 { + workers = 1 + } + continue + } + if strings.EqualFold(arg, "--skip-errors") { + log.Println("Flag: Skipping on Conversion Error") + featureSkipErrors = true + continue + } + flags = append(flags, arg) + } + if len(flags) < 3 { + fmt.Println("imageconvert") + fmt.Println(" --skip-errors - Skip on conversion error") + fmt.Println(" --skip-resume - Skip Resume Checking") + fmt.Println(" --multithread - Use Multiple Threads") + fmt.Println(" --recursive - Scan Directories Recursively") + fmt.Println(" - File Extension(s) to convert from, delimited with comma") + fmt.Println(" - File Extension to convert into") + fmt.Println(" [Arguments] - Arguments to pass onto ImageMagick") + os.Exit(0) + } + + // Scan Directory + scan([]string{}, strings.Split(flags[1], ",")) + + // Startup Workers + var consoleLock sync.Mutex + var awaitWorkers sync.WaitGroup + var itemsRemaining atomic.Int32 + itemsRemaining.Add(int32(len(queue))) + jobs := make(chan int, len(queue)) + + log.Printf("Queued Files: %d\n", len(queue)) + log.Printf("Worker Count: %d\n", workers) + + for workerID := 0; workerID < workers; workerID++ { + awaitWorkers.Add(1) + go func() { + defer awaitWorkers.Done() + for i := range jobs { + info := queue[i] + + // Generate Paths + directory := path.Join(info.Nest...) + srcPath := path.Join(directory, info.Filename) + dstPath := fmt.Sprint(path.Join(OUTPUT_DIR, directory, info.Basename), ".", flags[2]) + + if err := os.MkdirAll(path.Join(OUTPUT_DIR, directory), OUTPUT_FLAG); err != nil { + log.Fatalln("Cannot create output directory:", err) + } + + // Compile Arguments + args := make([]string, 0, len(flags)) + args = append(args, srcPath) + for i := 3; i < len(flags); i++ { + args = append(args, flags[i]) + } + args = append(args, dstPath) + proc := exec.Command("magick", args...) + + // Output Errors + if output, err := proc.CombinedOutput(); err != nil { + + consoleLock.Lock() + exitcode := -1 + if proc.ProcessState != nil { + exitcode = proc.ProcessState.ExitCode() + } + fmt.Printf("\r") + log.Printf("Processing Failed for '%s' with code: %d\n%s\n\n", + srcPath, exitcode, strings.TrimSpace(string(output))) + consoleLock.Unlock() + + if !featureSkipErrors { + os.Exit(1) + } + } + + // Output Progress + consoleLock.Lock() + fmt.Printf("\r ") + fmt.Printf("\rItems Left: %d", itemsRemaining.Add(-1)) + consoleLock.Unlock() + } + }() + } + + // Begin Processing + for i := 0; i < len(queue); i++ { + jobs <- i + } + close(jobs) + awaitWorkers.Wait() + + // Processing Complete + fmt.Printf("\n") + log.Printf("Processing Completed in %s\n", time.Since(t)) +} diff --git a/mangapub/go.mod b/mangapub/go.mod new file mode 100644 index 0000000..487b77e --- /dev/null +++ b/mangapub/go.mod @@ -0,0 +1,5 @@ +module github.com/bakonpancakz/clitools/mangapub + +go 1.24.0 + +require golang.org/x/image v0.33.0 diff --git a/mangapub/go.sum b/mangapub/go.sum new file mode 100644 index 0000000..ba0ff29 --- /dev/null +++ b/mangapub/go.sum @@ -0,0 +1,2 @@ +golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ= +golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc= diff --git a/mangapub/main.go b/mangapub/main.go new file mode 100644 index 0000000..d798286 --- /dev/null +++ b/mangapub/main.go @@ -0,0 +1,484 @@ +package main + +import ( + "archive/zip" + "bytes" + "crypto/rand" + "embed" + "fmt" + "image" + "image/color" + "image/gif" + "image/jpeg" + "image/png" + "io" + "log" + "math" + "os" + "path" + "runtime" + "sort" + "strconv" + "strings" + "sync" + "text/template" + "time" + + "golang.org/x/image/draw" + "golang.org/x/image/webp" +) + +type File struct { + Name string + Images []Image +} + +type Image struct { + Name string + Data []byte + MimeType string +} + +type QueuedItem struct { + Basename string // Filename without Extension + Filename string // Filename with Extension + Nest []string // Subdirectories +} + +const ( + OUTPUT_DIR = "convert" + OUTPUT_FLAG = 0755 +) + +var ( + featureRecursive bool = false + featureExtract bool = false + featureHeight int = 800 + featureWidth int = 600 + featureQuality int = 25 + flags []string + queue []QueuedItem +) + +//go:embed templates/* +var templateFS embed.FS + +func main() { + t := time.Now() + + // Parse Arguments + for i := 1; i < len(os.Args); i++ { + segments := strings.SplitN(os.Args[i], "=", 2) + if len(segments) == 2 { + n := segments[0] + s := segments[1] + switch { + case strings.EqualFold(n, "--height"): + v := parseInteger(n, s, 128, math.MaxInt) + log.Printf("Flag: Height %d\n", v) + featureHeight = v + + case strings.EqualFold(n, "--width"): + v := parseInteger(n, s, 128, math.MaxInt) + log.Printf("Flag: Width %d\n", v) + featureWidth = v + + case strings.EqualFold(n, "--quality"): + v := parseInteger(n, s, 0, 100) + log.Printf("Flag: Quality %d\n", v) + featureQuality = v + + default: + log.Printf("%s: Unknown Argument", n) + os.Exit(1) + } + + } else { + n := segments[0] + if strings.EqualFold(n, "--recursive") { + log.Println("Flag: Scanning Recursively") + featureRecursive = true + continue + } + if strings.EqualFold(n, "--extract") { + log.Println("Flag: Extracting Images") + featureExtract = true + continue + } + flags = append(flags, segments[0]) + } + } + if len(flags) < 1 { + fmt.Println("mangapub") + fmt.Println(" --extract - Extract Images to Directory") + fmt.Println(" --recursive - Scan Directories Recursively") + fmt.Println(" --height= - Image Height (Default: 800)") + fmt.Println(" --width= - Image Width (Default: 600)") + fmt.Println(" --quality= - JPEG Quality (Default: 25, Range: 0-100)") + fmt.Println(" - Directory to Scan (Use \".\" for current directory)") + os.Exit(0) + } + + // Process Archives + scan([]string{}) + for _, info := range queue { + + // Generate Paths + directory := path.Join(info.Nest...) + srcPath := path.Join(directory, info.Filename) + dstPath := path.Join(OUTPUT_DIR, directory, info.Basename) + if err := os.MkdirAll(path.Join(OUTPUT_DIR, directory), OUTPUT_FLAG); err != nil { + log.Fatalln("Cannot create output directory:", err) + } + log.Printf("Converting: %s\n", srcPath) + + // Convert Archive + contents, err := ParseCBZ(srcPath) + if err != nil { + log.Printf("Failed to parse CBZ '%s': %s\n", srcPath, err) + continue + } + if featureExtract { + if err := CreateDirectory(contents, dstPath); err != nil { + log.Printf("Failed to create DIR '%s': %s\n", dstPath, err) + continue + } + } else { + if err := CreateEPUB(contents, dstPath); err != nil { + log.Printf("Failed to create EPUB '%s': %s\n", dstPath, err) + continue + } + } + + } + + // Processing Complete + fmt.Printf("\n") + log.Printf("Processing Completed in %s\n", time.Since(t)) +} + +// Parse Integer for CLI Arguments +func parseInteger(n string, s string, min int, max int) int { + v, err := strconv.Atoi(s) + if err != nil { + fmt.Printf("%s: Not A Number\n", n) + os.Exit(1) + } + if v < min { + fmt.Printf("%s: Value cannot be less than %d\n", n, min) + os.Exit(1) + } + if v > max { + fmt.Printf("%s: Value cannot be more than %d\n", n, max) + os.Exit(1) + } + return v +} + +// Scan directory and append eligible items to queue +func scan(nesting []string) { + if len(nesting) == 1 && strings.EqualFold(nesting[0], OUTPUT_DIR) { + return + } + + // Read Entries in Directory + directory := path.Join(nesting...) + if directory == "" { + directory = path.Clean(flags[0]) + nesting = []string{flags[0]} + } + dirEntries, err := os.ReadDir(directory) + if err != nil { + log.Fatalf("Error reading directory '%s': %s\n", directory, err) + } + + for _, entry := range dirEntries { + fileName := entry.Name() + + // Scan Subdirectory + if entry.IsDir() { + if featureRecursive { + scan(append(nesting, fileName)) + } + continue + } + + // Add Matching File Extensions to Queue + fileExt := path.Ext(fileName) + if !strings.EqualFold(fileExt, ".cbz") { + continue + } + queue = append(queue, QueuedItem{ + Filename: fileName, + Basename: strings.TrimSuffix(fileName, fileExt), + Nest: nesting, + }) + } +} + +func GenerateUUID() string { + uuid := make([]byte, 16) + _, err := rand.Read(uuid) + if err != nil { + // Fallback to a timestamp-based ID if random generation fails + return fmt.Sprintf("%x", time.Now().UnixNano()) + } + + // Set version (4) and variant (RFC 4122) + uuid[6] = (uuid[6] & 0x0f) | 0x40 + uuid[8] = (uuid[8] & 0x3f) | 0x80 + return fmt.Sprintf("%x-%x-%x-%x-%x", + uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:16]) +} + +func ParseCBZ(filename string) (*File, error) { + + // CBZ files are really just zip archives + reader, err := zip.OpenReader(filename) + if err != nil { + return nil, fmt.Errorf("failed to open CBZ file: %w", err) + } + defer reader.Close() + + cbzFile := &File{ + Name: filename, + Images: []Image{}, + } + + // Multithreaded image processing + var wc = make(chan int, len(reader.File)) + var wg sync.WaitGroup + var wm sync.Mutex + for c := 0; c < runtime.NumCPU(); c++ { + wg.Add(1) + go func() { + defer wg.Done() + for i := range wc { + + // Ignore Directories + file := reader.File[i] + if file.FileInfo().IsDir() { + continue + } + + // Read file contents inside archive + rc, err := file.Open() + if err != nil { + log.Printf("failed to open file in CBZ: %s\n", err) + continue + } + d, _ := io.ReadAll(rc) + rc.Close() + + // Decode Image with the appropriate decoder based on it's starting bytes + // https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files) + var decoderImage image.Image + var decoderError error + switch { + case len(d) > 3 && // JPEG + d[0] == 0xFF && d[1] == 0xD8 && d[2] == 0xFF: + decoderImage, decoderError = jpeg.Decode(bytes.NewReader(d)) + + case len(d) > 8 && // PNG + d[0] == 0x89 && d[1] == 0x50 && d[2] == 0x4E && d[3] == 0x47 && + d[4] == 0x0D && d[5] == 0x0A && d[6] == 0x1A && d[7] == 0x0A: + decoderImage, decoderError = png.Decode(bytes.NewReader(d)) + + case len(d) > 4 && // GIF + d[0] == 0x47 && d[1] == 0x49 && d[2] == 0x46 && d[3] == 0x38: + decoderImage, decoderError = gif.Decode(bytes.NewReader(d)) + + case len(d) > 12 && // WEBP + d[0] == 0x52 && d[1] == 0x49 && d[2] == 0x46 && d[3] == 0x46 && + d[8] == 0x57 && d[9] == 0x45 && d[10] == 0x42 && d[11] == 0x50: + decoderImage, decoderError = webp.Decode(bytes.NewReader(d)) + + default: // unsupported content type + continue + } + if decoderError != nil { + log.Printf("malformed image: %s\n", err) + continue + } + + // Calculate Scaled Height and Width + bounds := decoderImage.Bounds() + targetW, targetH := featureWidth, featureHeight + iw, ih := bounds.Dx(), bounds.Dy() + ratio := math.Min(float64(targetW)/float64(iw), float64(targetH)/float64(ih)) + sw, sh := int(float64(iw)*ratio), int(float64(ih)*ratio) + canvas := image.NewRGBA(image.Rect(0, 0, targetW, targetH)) + + // Resize Image (White Background) + for x := 0; x < targetW; x++ { + for y := 0; y < targetH; y++ { + canvas.SetRGBA(x, y, color.RGBA{R: 255, G: 255, B: 255, A: 255}) + } + } + offsetX := (targetW - sw) / 2 + offsetY := (targetH - sh) / 2 + draw.CatmullRom.Scale(canvas, image.Rect(offsetX, offsetY, offsetX+sw, offsetY+sh), + decoderImage, bounds, draw.Over, nil) + + // Encode Resized Image into JPEG + enc := bytes.Buffer{} + if err := jpeg.Encode(&enc, canvas, &jpeg.Options{Quality: featureQuality}); err != nil { + log.Printf("encoding error: %s\n", err) + continue + } + + // Append Image to List + wm.Lock() + cbzFile.Images = append(cbzFile.Images, Image{ + Name: strings.TrimSuffix(path.Base(file.Name), path.Ext(file.Name)) + ".jpeg", + Data: enc.Bytes(), + MimeType: "image/jpeg", + }) + wm.Unlock() + } + }() + } + + // Wait for processing to complete + for i := 0; i < len(reader.File); i++ { + wc <- i + } + close(wc) + wg.Wait() + + // Sort Images by Name + sort.Slice(cbzFile.Images, func(i, j int) bool { + return cbzFile.Images[i].Name < cbzFile.Images[j].Name + }) + return cbzFile, nil +} + +func CreateEPUB(input *File, filename string) error { + + // EPUB files are really just zip archives + writer, err := os.Create(filename + ".epub") + if err != nil { + return fmt.Errorf("failed to create output file: %w", err) + } + defer writer.Close() + + archive := zip.NewWriter(writer) + defer archive.Close() + + { + // Write Mime Header + mimetype, err := archive.CreateHeader(&zip.FileHeader{ + Name: "mimetype", + Method: zip.Store, + }) + if err != nil { + return fmt.Errorf("failed to create mimetype file: %w", err) + } + if _, err = mimetype.Write([]byte("application/epub+zip")); err != nil { + return fmt.Errorf("failed to write mimetype file: %w", err) + } + } + + type Item struct { + ID int + Base string + Type string + } + var ( + ContentTitle = strings.TrimSuffix(path.Base(input.Name), path.Ext(input.Name)) + ContentDate = time.Now().Format("2006-01-02") + ContentUUID = GenerateUUID() + ContentImages = make([]Item, 0, len(input.Images)) + ) + for i, image := range input.Images { + + // Create Metadata Entry + pathBase := fmt.Sprintf("page%03d", i+1) + pathItem := Item{ + ID: i + 1, + Base: pathBase, + Type: image.MimeType, + } + + // Add HTML to Archive + { + pathOutput := fmt.Sprint("OEBPS/pages/", pathBase, ".xhtml") + pathTemplate := "templates/page.xml" + tmpl, err := template.ParseFS(templateFS, pathTemplate) + if err != nil { + return fmt.Errorf("cannot open template file '%s': %s", pathTemplate, err) + } + output, err := archive.Create(pathOutput) + if err != nil { + return fmt.Errorf("cannot create archive file '%s': %s", pathOutput, err) + } + if err := tmpl.Execute(output, pathItem); err != nil { + return fmt.Errorf("cannot execute template file '%s': %s", pathTemplate, err) + } + } + + // Add Image to Archive + { + pathOutput := fmt.Sprint("OEBPS/images/", pathBase, ".jpeg") + output, err := archive.Create(pathOutput) + if err != nil { + return fmt.Errorf("cannot create archive file '%s': %s", pathOutput, err) + } + if _, err = output.Write(image.Data); err != nil { + return fmt.Errorf("cannot write archive file '%s': %s", pathOutput, err) + } + } + + ContentImages = append(ContentImages, pathItem) + } + + { + // Generate Metadata with Templates + literals := map[string]any{ + "ContentTitle": ContentTitle, + "ContentDate": ContentDate, + "ContentUUID": ContentUUID, + "ContentImages": ContentImages, + } + for _, meta := range [][]string{ + {"OEBPS/content.opf", "templates/content.opf"}, + {"OEBPS/toc.ncx", "templates/toc.ncx"}, + {"META-INF/container.xml", "templates/container.xml"}, + } { + pathOutput := meta[0] + pathTemplate := meta[1] + tmpl, err := template.ParseFS(templateFS, pathTemplate) + if err != nil { + return fmt.Errorf("cannot open template file '%s': %s", pathTemplate, err) + } + output, err := archive.Create(pathOutput) + if err != nil { + return fmt.Errorf("cannot create archive file '%s': %s", pathOutput, err) + } + if err := tmpl.Execute(output, literals); err != nil { + return fmt.Errorf("cannot execute template file '%s': %s", pathTemplate, err) + } + } + } + + return nil +} + +func CreateDirectory(input *File, filename string) error { + + // Create Output Directory + if err := os.MkdirAll(filename, OUTPUT_FLAG); err != nil { + return fmt.Errorf("failed to create output dir: %w", err) + } + + // Write Images + for i, image := range input.Images { + imageName := fmt.Sprintf("page%03d.jpeg", i+1) + imagePath := path.Join(filename, imageName) + if err := os.WriteFile(imagePath, image.Data, OUTPUT_FLAG); err != nil { + return fmt.Errorf("failed to write image: %w", err) + } + } + + return nil +} diff --git a/mangapub/templates/container.xml b/mangapub/templates/container.xml new file mode 100644 index 0000000..173afcc --- /dev/null +++ b/mangapub/templates/container.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/mangapub/templates/content.opf b/mangapub/templates/content.opf new file mode 100644 index 0000000..092a10c --- /dev/null +++ b/mangapub/templates/content.opf @@ -0,0 +1,24 @@ + + + + {{ .ContentTitle }} + en + urn:uuid:{{ .ContentUUID }} + {{ .ContentDate }} + bakonpancakz + + + + {{ range .ContentImages }} + + {{ end }} + {{ range .ContentImages }} + + {{ end }} + + + {{ range .ContentImages }} + + {{ end }} + + \ No newline at end of file diff --git a/mangapub/templates/page.xml b/mangapub/templates/page.xml new file mode 100644 index 0000000..e6d37d9 --- /dev/null +++ b/mangapub/templates/page.xml @@ -0,0 +1,16 @@ + + + + + Page {{ .ID }} + + + +
+ Page {{ .ID }} +
+ + \ No newline at end of file diff --git a/mangapub/templates/toc.ncx b/mangapub/templates/toc.ncx new file mode 100644 index 0000000..a1852dc --- /dev/null +++ b/mangapub/templates/toc.ncx @@ -0,0 +1,23 @@ + + + + + + + + + + + {{ .ContentTitle }} + + + {{ range .ContentImages }} + + + Page {{ .ID }} + + + + {{ end }} + + \ No newline at end of file diff --git a/mediaconvert/main.go b/mediaconvert/main.go new file mode 100644 index 0000000..79d32ee --- /dev/null +++ b/mediaconvert/main.go @@ -0,0 +1,329 @@ +package main + +import ( + "bufio" + "bytes" + "fmt" + "log" + "os" + "os/exec" + "path" + "runtime" + "strconv" + "strings" + "sync" + "sync/atomic" + "syscall" + "time" + "unsafe" +) + +type QueuedItem struct { + Basename string // Filename without Extension + Filename string // Filename with Extension + Nest []string // Subdirectories +} + +var ( + OUTPUT_FLAG = os.FileMode(0666) + OUTPUT_DIR = "convert" + featureResume bool = true + featureRecursive bool = false + logLines []string + logMutex sync.Mutex + flags []string + queue []QueuedItem + itemsRemaining atomic.Int32 + awaitWorkers sync.WaitGroup + workers = 1 +) + +func defaultField(m map[string]string, key string) string { + if val, ok := m[key]; ok { + return val + } else { + return "" + } +} + +// Warning, magic numbers... really janky... + +func logUpdate(index int, message string) { + logMutex.Lock() + logLines[index+1] = fmt.Sprintf("[%02d] %s", index, message) + logMutex.Unlock() +} + +func logRenderer() { + logLines = make([]string, workers+3) + for i := 0; i < len(logLines); i++ { + fmt.Println() + } + for { + logMutex.Lock() + + // Footer + logLines[len(logLines)-1] = fmt.Sprintf("\rItems Left: %d\n", itemsRemaining.Load()) + + // Log Lines + fmt.Printf("\033[%dA", len(logLines)+1) + for _, line := range logLines { + fmt.Printf("\033[2K\r%-80s\n", line) + } + fmt.Printf("\r") + + logMutex.Unlock() + time.Sleep(100 * time.Millisecond) + } +} + +func scan(nest []string, extensions []string) { + if len(nest) == 1 && strings.EqualFold(nest[0], OUTPUT_DIR) { + return + } + folder := path.Join(nest...) + if folder == "" { + folder = "." + } + files, err := os.ReadDir(folder) + if err != nil { + log.Fatalf("Error reading directory '%s': %s\n", folder, err) + } + for _, entry := range files { + filename := entry.Name() + + // Scan Subdirectory + if entry.IsDir() { + if featureRecursive { + scan(append(nest, filename), extensions) + } + continue + } + + // Match File Extension + for _, prefix := range extensions { + if len(filename) < len(prefix) { + continue + } + if !strings.EqualFold(filename[len(filename)-len(prefix):], prefix) { + continue + } + + // Perform Resume Check + basename := filename[:strings.LastIndex(filename, ".")] + location := fmt.Sprint(path.Join(OUTPUT_DIR, folder, basename), ".", flags[2]) + if featureResume { + if info, err := os.Stat(location); err == nil { + if info.Size() != 0 { + fmt.Printf("Skipping '%s' as it is already complete\n", filename) + continue + } + } + } + + // Add Item to Queue + queue = append(queue, QueuedItem{ + Filename: filename, + Basename: basename, + Nest: nest, + }) + break + } + } +} + +func main() { + t := time.Now() + + // Enable ANSI escape codes on Windows 10+ + // I don't remember where I copied this from, sorry... (>_>) + if runtime.GOOS == "windows" { + stdout := os.Stdout.Fd() + var mode uint32 + proc := syscall.NewLazyDLL("kernel32.dll").NewProc("GetConsoleMode") + proc.Call(stdout, uintptr(unsafe.Pointer(&mode))) + mode |= 0x0004 // ENABLE_VIRTUAL_TERMINAL_PROCESSING + proc = syscall.NewLazyDLL("kernel32.dll").NewProc("SetConsoleMode") + proc.Call(stdout, uintptr(mode)) + } + + // Collect Arguments + for _, arg := range os.Args { + before, after, _ := strings.Cut(arg, "=") + if strings.EqualFold(before, "--skip-resume") { + log.Println("Flag: Disabling Resume Check") + featureResume = false + continue + } + if strings.EqualFold(before, "--recursive") { + log.Println("Flag: Scanning Recursively") + featureRecursive = true + continue + } + if strings.EqualFold(before, "--multithread") { + log.Println("Flag: Enabling Multi-threading") + workers = runtime.NumCPU() + continue + } + if strings.EqualFold(before, "--output") { + info, err := os.Stat(after) + if err == nil { + if !info.IsDir() { + err = fmt.Errorf("Not a directory") + } + } + if err != nil { + log.Fatalln("Invalid Output Directory:", err) + return + } + log.Println("Flag: Setting Output Directory:", after) + OUTPUT_DIR = after + continue + } + flags = append(flags, arg) + } + if len(flags) < 3 { + fmt.Println("mediaconvert") + fmt.Println(" --skip-resume - Skip Resume Checking") + fmt.Println(" --multithread - Use Multiple Threads") + fmt.Println(" --recursive - Scan Directories Recursively") + fmt.Println(" --output={...} - Set Output Directory") + fmt.Println(" - File Extension(s) to convert from, delimited with comma") + fmt.Println(" - File Extension to convert into") + fmt.Println(" [Arguments] - Arguments to pass onto FFMPEG") + fmt.Println("Templates:") + fmt.Println(" {filename} - Full Filename (e.g. myfile.txt)") + fmt.Println(" {basename} - Base Filename (e.g. myfile") + fmt.Println(" {directory} - Source Directory (e.g. /path/to/file)") + os.Exit(0) + } + + // Scan Directory + scan([]string{}, strings.Split(flags[1], ",")) + + // Startup Workers + log.Printf("Queued Files: %d\n", len(queue)) + log.Printf("Worker Count: %d\n", workers) + jobs := make(chan int, len(queue)) + itemsRemaining.Add(int32(len(queue))) + + if len(queue) > 0 { + go logRenderer() + } + + for i := 0; i < workers; i++ { + awaitWorkers.Add(1) + go func(workerID int) { + defer awaitWorkers.Done() + for i := range jobs { + info := queue[i] + s := time.Now() + + // Generate Paths + directory := path.Join(info.Nest...) + srcPath := path.Join(directory, info.Filename) + dstPath := fmt.Sprint(path.Join(OUTPUT_DIR, directory, info.Basename), ".", flags[2]) + + if err := os.MkdirAll(path.Join(OUTPUT_DIR, directory), OUTPUT_FLAG); err != nil { + log.Fatalln("Cannot create output directory:", err) + } + + // Compile Arguments + args := []string{"-hide_banner", "-y", "-progress", "-", "-i", srcPath} + for i := 3; i < len(flags); i++ { + str := flags[i] + str = strings.ReplaceAll(str, "{basename}", info.Basename) + str = strings.ReplaceAll(str, "{filename}", info.Filename) + str = strings.ReplaceAll(str, "{directory}", path.Join(info.Nest...)) + args = append(args, str) + } + args = append(args, dstPath) + proc := exec.Command("ffmpeg", args...) + + // Collect Error Output + errors := bytes.Buffer{} + stderr, err := proc.StderrPipe() + if err != nil { + log.Fatalf("Failed to open Error Output: %s\n", err) + } + go func() { + scanner := bufio.NewScanner(stderr) + for scanner.Scan() { + errors.Write(scanner.Bytes()) + errors.WriteRune('\n') + } + }() + + // Collect Progress Output + output, err := proc.StdoutPipe() + if err != nil { + log.Fatalf("Failed to open Standard Output: %s\n", err) + } + go func() { + for { + buffer := make([]byte, 256) + r, err := output.Read(buffer) + if err != nil { + break + } + progress := map[string]string{} + metadata := strings.Split(string(buffer[:r]), "\n") + for _, line := range metadata { + values := strings.Split(line, "=") + if len(values) == 2 { + key := strings.TrimSpace(values[0]) + val := strings.TrimSpace(values[1]) + progress[key] = val + } + } + switch defaultField(progress, "progress") { + case "continue": + // Clear Output and Display Progress + sizeTotal := defaultField(progress, "total_size") + sizeFloat, _ := strconv.ParseFloat(sizeTotal, 64) + sizeValue := strconv.FormatFloat(sizeFloat/1024/1024, 'f', 2, 64) + logUpdate(workerID, fmt.Sprintf( + "Time: %s, Bitrate: %s, FPS: %s/%s/%s, Size: %sMB (%s)", + defaultField(progress, "out_time"), + defaultField(progress, "bitrate"), + defaultField(progress, "fps"), + defaultField(progress, "drop_frames"), + defaultField(progress, "dup_frames"), + sizeValue, + defaultField(progress, "speed"), + )) + case "end": + // Clear Output and Display Completion Time + logUpdate(workerID, fmt.Sprintf( + "Processing Completed in %s", + time.Since(s), + )) + } + } + }() + + // Start Processing + if err := proc.Run(); err != nil { + exitcode := -1 + if proc.ProcessState != nil { + exitcode = proc.ProcessState.ExitCode() + } + log.Printf("Processing Failed for '%s' with code: %d\n%s\n\n", + srcPath, exitcode, errors.String()) + os.Exit(1) + } + itemsRemaining.Add(-1) + } + }(i) + } + + // Begin Processing + for i := 0; i < len(queue); i++ { + jobs <- i + } + close(jobs) + awaitWorkers.Wait() + + // Processing Complete + log.Printf("Processing Completed in %s\n", time.Since(t)) +}