From c160a0a24a2b709348aececd4ee3292c77bf90ef Mon Sep 17 00:00:00 2001 From: Joren Date: Wed, 2 Apr 2025 21:52:46 +0200 Subject: [PATCH] Verbose --- main.go | 97 +++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 80 insertions(+), 17 deletions(-) diff --git a/main.go b/main.go index 4ff177c..87cdbeb 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( "regexp" "strconv" "strings" + "sync" "time" ) @@ -26,20 +27,31 @@ type FileQuality struct { ReleaseInfo string // Human-readable release info for logging } +type ProcessResult struct { + BaseFile string + SpaceRecovered int64 + Processed bool +} + var ( - logFile *os.File - armed bool - dryRun bool - forceMode bool // Force processing even if releases appear different - logWriter *bufio.Writer - version = "1.1.0" - dateFormat = "2006-01-02 15:04:05" + logFile *os.File + armed bool + dryRun bool + forceMode bool // Force processing even if releases appear different + verbose bool // Control console output + concurrency int // Number of concurrent goroutines + logWriter *bufio.Writer + version = "1.2.0" + dateFormat = "2006-01-02 15:04:05" + logMutex sync.Mutex ) func init() { flag.BoolVar(&armed, "armed", false, "Enable actual file operations (rename/delete)") flag.BoolVar(&dryRun, "dry-run", false, "Simulate operations without making changes") flag.BoolVar(&forceMode, "force", false, "Force processing even if releases appear different") + flag.BoolVar(&verbose, "verbose", false, "Show detailed logs on console") + flag.IntVar(&concurrency, "concurrency", 4, "Number of concurrent file groups to process") flag.Usage = func() { fmt.Fprintf(flag.CommandLine.Output(), "FLAC Duplicate Cleaner v%s\n", version) fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] [directory]\n", os.Args[0]) @@ -47,6 +59,7 @@ func init() { fmt.Println("\nBy default, runs in dry-run mode showing what would be done") fmt.Println("Specify -armed to actually perform operations") fmt.Println("Use -force to process files even if they appear to be from different releases") + fmt.Println("Add -verbose to show detailed logs on console") } } @@ -57,16 +70,33 @@ func initLogging(logFilename string) error { return fmt.Errorf("failed to open log file: %w", err) } logWriter = bufio.NewWriter(logFile) - log.SetOutput(io.MultiWriter(os.Stdout, logWriter)) + + // Set log output based on verbose flag + if verbose { + log.SetOutput(io.MultiWriter(os.Stdout, logWriter)) + } else { + log.SetOutput(logWriter) + } + return nil } func closeLogging() { + logMutex.Lock() + defer logMutex.Unlock() logWriter.Flush() logFile.Close() } func writeLog(message string) { + logMutex.Lock() + defer logMutex.Unlock() + log.Println(message) +} + +func printFinalStats(message string) { + // Always print final stats to console regardless of verbose setting + fmt.Println(message) log.Println(message) } @@ -148,6 +178,7 @@ func getReleaseInfo(filePath string) string { func findDuplicateFiles(rootDir string) (map[string][]FileQuality, error) { dupePattern := regexp.MustCompile(`(?i)(.+)( \(\d+\))\.flac$`) fileGroups := make(map[string][]FileQuality) + var filesMutex sync.Mutex err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { if err != nil { @@ -165,6 +196,9 @@ func findDuplicateFiles(rootDir string) (map[string][]FileQuality, error) { // Check if file matches the duplicate pattern matches := dupePattern.FindStringSubmatch(path) + filesMutex.Lock() + defer filesMutex.Unlock() + if len(matches) > 1 { baseFile := matches[1] + ".flac" fileInfo, err := os.Stat(path) @@ -384,6 +418,7 @@ func main() { if forceMode { writeLog("FORCE MODE: Will process even if releases appear different!") } + writeLog(fmt.Sprintf("CONCURRENCY: Processing %d file groups simultaneously", concurrency)) writeLog("===============================") // Check mediainfo is available @@ -406,20 +441,48 @@ func main() { writeLog("") } + // Process file groups concurrently with limited goroutines + var wg sync.WaitGroup + results := make(chan ProcessResult, len(fileGroups)) + semaphore := make(chan struct{}, concurrency) + + for baseFile, group := range fileGroups { + wg.Add(1) + go func(bf string, g []FileQuality) { + defer wg.Done() + semaphore <- struct{}{} // Acquire semaphore + defer func() { <-semaphore }() // Release semaphore + + spaceRecovered, processed := processFileGroup(bf, g) + results <- ProcessResult{ + BaseFile: bf, + SpaceRecovered: spaceRecovered, + Processed: processed, + } + }(baseFile, group) + } + + // Close results channel once all goroutines complete + go func() { + wg.Wait() + close(results) + }() + + // Process results var totalRecoverableSpace int64 processedGroups := 0 - for baseFile, group := range fileGroups { - spaceRecovered, processed := processFileGroup(baseFile, group) - if processed { + for result := range results { + if result.Processed { processedGroups++ - totalRecoverableSpace += spaceRecovered + totalRecoverableSpace += result.SpaceRecovered } - writeLog("") } - writeLog(fmt.Sprintf("Processed %d of %d file groups", processedGroups, len(fileGroups))) - writeLog(fmt.Sprintf("Total recoverable space: %s", bytesToHumanReadable(totalRecoverableSpace))) - writeLog(fmt.Sprintf("Cleanup completed at %s", getCurrentTime())) - writeLog("===============================") + // Final stats - these always print to console regardless of verbose setting + finalTime := getCurrentTime() + printFinalStats(fmt.Sprintf("Processed %d of %d file groups", processedGroups, len(fileGroups))) + printFinalStats(fmt.Sprintf("Total recoverable space: %s", bytesToHumanReadable(totalRecoverableSpace))) + printFinalStats(fmt.Sprintf("Cleanup completed at %s", finalTime)) + printFinalStats("===============================") }