package main import ( _ "embed" "io/fs" "os" "path/filepath" "runtime" "strconv" "strings" "sync" "time" "git.velvettear.de/velvettear/id3tool/internal/config" "git.velvettear.de/velvettear/loggo" "github.com/bogem/id3v2/v2" ) var taskLimiter = make(chan struct{}, runtime.NumCPU()) var taskCommunication = make(chan task) var waitGroup = sync.WaitGroup{} var counter int var taskState = make(map[int]task) func main() { loggo.SetLogFormat("[$LOGLEVEL$] > $MESSAGE$ ($EXTRAS$) [$TIMEDIFF$]") config.CheckArguments() if len(config.Directory) == 0 { loggo.Fatal("no directory provided") } stats, err := os.Stat(config.Directory) if err != nil { loggo.Fatal("could not get stats for provided directory", err.Error()) } if !stats.IsDir() { loggo.Fatal("provided argument is not a valid directory") } filepath.WalkDir(config.Directory, filterValidFiles) waitGroup.Wait() } func filterValidFiles(path string, dir fs.DirEntry, err error) error { if err != nil { return err } extension := filepath.Ext(path) if dir.IsDir() || (extension != ".flac" && extension != ".mp3") { return nil } counter++ waitGroup.Add(1) taskLimiter <- struct{}{} tmp := task{counter, path, false} go func(tmp task) { go func(tmp task) { time.Sleep(time.Duration(config.Timeout) * time.Second) tmp.timeout = true taskCommunication <- tmp <-taskLimiter }(tmp) cleanFrames(tmp.id, tmp.file) tmp.timeout = false taskCommunication <- tmp <-taskLimiter }(tmp) result := <-taskCommunication check := taskState[result.id] if check.id == 0 { taskState[result.id] = result if result.timeout { loggo.Warning("(" + strconv.Itoa(result.id) + ") cleaning of file's '" + result.file + "' id3 tags timed out") } } waitGroup.Done() return nil } func cleanFrames(id int, file string) { if len(file) == 0 { return } timestamp := time.Now() loggo.Info("(" + strconv.Itoa(id) + ") cleaning file's '" + file + "' id3 tags...") tag, err := id3v2.Open(file, id3v2.Options{Parse: true}) if err != nil { loggo.Error("("+strconv.Itoa(id)+") encountered an error opening the file '"+file+"' for reading the id3 tags", err.Error()) return } defer tag.Close() deleted := 0 for key := range tag.AllFrames() { filter := frameFilter(key) if !filter { continue } tag.DeleteFrames(key) deleted++ } if deleted > 0 { loggo.Debug("(" + strconv.Itoa(id) + ") saving modified id3 tags (" + strconv.Itoa(deleted) + " deleted) to file '" + file + "'...") err := tag.Save() if err != nil { loggo.Error("("+strconv.Itoa(id)+") encountered an error saving the modified id3 tags to file '"+file+"'", err.Error()) return } loggo.DebugTimed("("+strconv.Itoa(id)+") deleted "+strconv.Itoa(deleted)+" tag(s) from file '"+file+"'", timestamp.UnixMilli()) } loggo.DebugTimed("("+strconv.Itoa(id)+") finished cleaning file's '"+file+"' id3 tags", timestamp.UnixMilli()) } func frameFilter(frame string) bool { for _, frameToKeep := range config.FramesToKeep { if strings.EqualFold(frameToKeep, frame) { return false } } return true } type task struct { id int file string timeout bool }