This commit is contained in:
Joren 2025-04-02 21:52:46 +02:00
parent b1eed73dec
commit c160a0a24a
Signed by: Joren
GPG Key ID: 280E33DFBC0F1B55

97
main.go
View File

@ -12,6 +12,7 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
) )
@ -26,20 +27,31 @@ type FileQuality struct {
ReleaseInfo string // Human-readable release info for logging ReleaseInfo string // Human-readable release info for logging
} }
type ProcessResult struct {
BaseFile string
SpaceRecovered int64
Processed bool
}
var ( var (
logFile *os.File logFile *os.File
armed bool armed bool
dryRun bool dryRun bool
forceMode bool // Force processing even if releases appear different forceMode bool // Force processing even if releases appear different
logWriter *bufio.Writer verbose bool // Control console output
version = "1.1.0" concurrency int // Number of concurrent goroutines
dateFormat = "2006-01-02 15:04:05" logWriter *bufio.Writer
version = "1.2.0"
dateFormat = "2006-01-02 15:04:05"
logMutex sync.Mutex
) )
func init() { func init() {
flag.BoolVar(&armed, "armed", false, "Enable actual file operations (rename/delete)") flag.BoolVar(&armed, "armed", false, "Enable actual file operations (rename/delete)")
flag.BoolVar(&dryRun, "dry-run", false, "Simulate operations without making changes") 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(&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() { flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "FLAC Duplicate Cleaner v%s\n", version) 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]) 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("\nBy default, runs in dry-run mode showing what would be done")
fmt.Println("Specify -armed to actually perform operations") 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("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) return fmt.Errorf("failed to open log file: %w", err)
} }
logWriter = bufio.NewWriter(logFile) 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 return nil
} }
func closeLogging() { func closeLogging() {
logMutex.Lock()
defer logMutex.Unlock()
logWriter.Flush() logWriter.Flush()
logFile.Close() logFile.Close()
} }
func writeLog(message string) { 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) log.Println(message)
} }
@ -148,6 +178,7 @@ func getReleaseInfo(filePath string) string {
func findDuplicateFiles(rootDir string) (map[string][]FileQuality, error) { func findDuplicateFiles(rootDir string) (map[string][]FileQuality, error) {
dupePattern := regexp.MustCompile(`(?i)(.+)( \(\d+\))\.flac$`) dupePattern := regexp.MustCompile(`(?i)(.+)( \(\d+\))\.flac$`)
fileGroups := make(map[string][]FileQuality) fileGroups := make(map[string][]FileQuality)
var filesMutex sync.Mutex
err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
@ -165,6 +196,9 @@ func findDuplicateFiles(rootDir string) (map[string][]FileQuality, error) {
// Check if file matches the duplicate pattern // Check if file matches the duplicate pattern
matches := dupePattern.FindStringSubmatch(path) matches := dupePattern.FindStringSubmatch(path)
filesMutex.Lock()
defer filesMutex.Unlock()
if len(matches) > 1 { if len(matches) > 1 {
baseFile := matches[1] + ".flac" baseFile := matches[1] + ".flac"
fileInfo, err := os.Stat(path) fileInfo, err := os.Stat(path)
@ -384,6 +418,7 @@ func main() {
if forceMode { if forceMode {
writeLog("FORCE MODE: Will process even if releases appear different!") writeLog("FORCE MODE: Will process even if releases appear different!")
} }
writeLog(fmt.Sprintf("CONCURRENCY: Processing %d file groups simultaneously", concurrency))
writeLog("===============================") writeLog("===============================")
// Check mediainfo is available // Check mediainfo is available
@ -406,20 +441,48 @@ func main() {
writeLog("") 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 var totalRecoverableSpace int64
processedGroups := 0 processedGroups := 0
for baseFile, group := range fileGroups { for result := range results {
spaceRecovered, processed := processFileGroup(baseFile, group) if result.Processed {
if processed {
processedGroups++ processedGroups++
totalRecoverableSpace += spaceRecovered totalRecoverableSpace += result.SpaceRecovered
} }
writeLog("")
} }
writeLog(fmt.Sprintf("Processed %d of %d file groups", processedGroups, len(fileGroups))) // Final stats - these always print to console regardless of verbose setting
writeLog(fmt.Sprintf("Total recoverable space: %s", bytesToHumanReadable(totalRecoverableSpace))) finalTime := getCurrentTime()
writeLog(fmt.Sprintf("Cleanup completed at %s", getCurrentTime())) printFinalStats(fmt.Sprintf("Processed %d of %d file groups", processedGroups, len(fileGroups)))
writeLog("===============================") printFinalStats(fmt.Sprintf("Total recoverable space: %s", bytesToHumanReadable(totalRecoverableSpace)))
printFinalStats(fmt.Sprintf("Cleanup completed at %s", finalTime))
printFinalStats("===============================")
} }