diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b4077c4 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module cbr2cbz + +go 1.25.3 + +require github.com/nwaples/rardecode v1.1.3 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..677800e --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc= +github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= diff --git a/main.go b/main.go new file mode 100644 index 0000000..85257ba --- /dev/null +++ b/main.go @@ -0,0 +1,257 @@ +package main + +import ( + "archive/zip" + "bufio" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/nwaples/rardecode" +) + +type ConversionStats struct { + Successful int + Skipped int + Failed int + Total int +} + +func main() { + fmt.Println("CBR to CBZ Converter") + fmt.Println(strings.Repeat("=", 60)) + + // Get current directory + currentDir, err := os.Getwd() + if err != nil { + fmt.Printf("Error getting current directory: %v\n", err) + return + } + + // Find all .cbr files + cbrFiles, err := filepath.Glob(filepath.Join(currentDir, "*.cbr")) + if err != nil { + fmt.Printf("Error searching for .cbr files: %v\n", err) + return + } + + if len(cbrFiles) == 0 { + fmt.Println("No .cbr files found in current directory.") + return + } + + fmt.Printf("Found %d CBR file(s) to convert\n", len(cbrFiles)) + fmt.Println(strings.Repeat("=", 60)) + + // Process each file + stats := ConversionStats{Total: len(cbrFiles)} + + for _, cbrPath := range cbrFiles { + result := convertCBRtoCBZ(cbrPath) + switch result { + case "success": + stats.Successful++ + case "skipped": + stats.Skipped++ + case "failed": + stats.Failed++ + } + } + + // Print summary + fmt.Println() + fmt.Println(strings.Repeat("=", 60)) + fmt.Println("Conversion Summary:") + fmt.Printf(" ✓ Successful: %d\n", stats.Successful) + fmt.Printf(" ⚠ Skipped: %d\n", stats.Skipped) + fmt.Printf(" ✗ Failed: %d\n", stats.Failed) + fmt.Printf(" Total: %d\n", stats.Total) + + // Ask about deletion + if stats.Successful > 0 { + fmt.Println() + fmt.Println(strings.Repeat("=", 60)) + fmt.Print("Delete original .cbr files? (yes/no): ") + + reader := bufio.NewReader(os.Stdin) + response, _ := reader.ReadString('\n') + response = strings.TrimSpace(strings.ToLower(response)) + + if response == "yes" || response == "y" { + fmt.Println("\nDeleting original files...") + for _, cbrPath := range cbrFiles { + cbzPath := strings.TrimSuffix(cbrPath, ".cbr") + ".cbz" + if _, err := os.Stat(cbzPath); err == nil { + if err := os.Remove(cbrPath); err != nil { + fmt.Printf(" ✗ Failed to delete %s: %v\n", filepath.Base(cbrPath), err) + } else { + fmt.Printf(" ✓ Deleted: %s\n", filepath.Base(cbrPath)) + } + } + } + fmt.Println("\nDone!") + } else { + fmt.Println("\nOriginal files kept.") + } + } +} + +func convertCBRtoCBZ(cbrPath string) string { + cbzPath := strings.TrimSuffix(cbrPath, ".cbr") + ".cbz" + baseName := filepath.Base(cbrPath) + + fmt.Printf("\nProcessing: %s\n", baseName) + + // Check if output file already exists + if _, err := os.Stat(cbzPath); err == nil { + fmt.Printf(" ⚠ Skipping: %s already exists\n", filepath.Base(cbzPath)) + return "skipped" + } + + // Create temporary directory + tempDir, err := os.MkdirTemp("", "cbr2cbz-*") + if err != nil { + fmt.Printf(" ✗ Error creating temp directory: %v\n", err) + return "failed" + } + defer os.RemoveAll(tempDir) + + // Extract RAR file + fmt.Println(" → Extracting...") + extractedFiles, err := extractRAR(cbrPath, tempDir) + if err != nil { + fmt.Printf(" ✗ Error extracting: %v\n", err) + return "failed" + } + + if len(extractedFiles) == 0 { + fmt.Println(" ✗ Error: No files found in archive") + return "failed" + } + + fmt.Printf(" → Creating CBZ with %d files...\n", len(extractedFiles)) + + // Create CBZ file + err = createCBZ(cbzPath, tempDir, extractedFiles) + if err != nil { + fmt.Printf(" ✗ Error creating CBZ: %v\n", err) + os.Remove(cbzPath) + return "failed" + } + + // Get file sizes + originalInfo, _ := os.Stat(cbrPath) + newInfo, _ := os.Stat(cbzPath) + + originalSize := float64(originalInfo.Size()) / 1024 / 1024 + newSize := float64(newInfo.Size()) / 1024 / 1024 + ratio := (1 - float64(newInfo.Size())/float64(originalInfo.Size())) * 100 + + fmt.Printf(" ✓ Created: %s\n", filepath.Base(cbzPath)) + fmt.Printf(" Original: %.2f MB\n", originalSize) + fmt.Printf(" New: %.2f MB\n", newSize) + fmt.Printf(" Change: %+.1f%%\n", ratio) + + return "success" +} + +func extractRAR(rarPath, destDir string) ([]string, error) { + var extractedFiles []string + + // Open RAR file + r, err := rardecode.OpenReader(rarPath, "") + if err != nil { + return nil, err + } + defer r.Close() + + // Extract each file + for { + header, err := r.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + // Skip directories and hidden files + if header.IsDir || strings.HasPrefix(filepath.Base(header.Name), ".") { + continue + } + + // Create destination path + destPath := filepath.Join(destDir, header.Name) + + // Create directories if needed + if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil { + return nil, err + } + + // Create file + outFile, err := os.Create(destPath) + if err != nil { + return nil, err + } + + // Copy content + _, err = io.Copy(outFile, r) + outFile.Close() + if err != nil { + return nil, err + } + + extractedFiles = append(extractedFiles, header.Name) + } + + return extractedFiles, nil +} + +func createCBZ(cbzPath, sourceDir string, files []string) error { + // Create ZIP file with best compression + outFile, err := os.Create(cbzPath) + if err != nil { + return err + } + defer outFile.Close() + + zipWriter := zip.NewWriter(outFile) + defer zipWriter.Close() + + // Sort files for consistent ordering + sort.Strings(files) + + // Add each file to ZIP + for _, file := range files { + // Create ZIP entry with best compression + header := &zip.FileHeader{ + Name: file, + Method: zip.Deflate, + } + header.SetMode(0644) + + writer, err := zipWriter.CreateHeader(header) + if err != nil { + return err + } + + // Open source file + sourcePath := filepath.Join(sourceDir, file) + sourceFile, err := os.Open(sourcePath) + if err != nil { + return err + } + + // Copy to ZIP + _, err = io.Copy(writer, sourceFile) + sourceFile.Close() + if err != nil { + return err + } + } + + return nil +} \ No newline at end of file