diff --git a/OBM.go b/OBM.go new file mode 100644 index 0000000..1b8146d --- /dev/null +++ b/OBM.go @@ -0,0 +1,133 @@ +package main + +import ( + "errors" + "fmt" + "image" + "image/color" + "image/png" + "os" + "path/filepath" + "sync" + "time" + + "github.com/Unbewohnte/OBM/logger" + "github.com/Unbewohnte/OBM/manager" + "github.com/Unbewohnte/OBM/settings" +) + +var ( + WG sync.WaitGroup +) + +// creates a complete black image file +func createBlackBG(width, height int) error { + bgfile, err := os.Create("blackBG.png") + if err != nil { + return errors.New(fmt.Sprintf("Could not create black background file : %s", err)) + } + image := image.NewRGBA(image.Rect(0, 0, width, height)) + bounds := image.Bounds() + + for y := 0; y < bounds.Max.Y; y++ { + for x := 0; x < bounds.Max.X; x++ { + image.Set(x, y, color.Black) + } + } + err = png.Encode(bgfile, image) + if err != nil { + return errors.New(fmt.Sprintf("Could not encode an image : %s", err)) + } + err = bgfile.Close() + if err != nil { + return errors.New(fmt.Sprintf("Could not close the background file : %s", err)) + } + + return nil +} + +// a basic implementation of a concurrent worker +func worker(paths <-chan string, replacementImage string, successful, failed *uint64, WG *sync.WaitGroup) { + defer WG.Done() + for songPath := range paths { + s, f := manager.ReplaceBackgrounds(songPath, replacementImage) + *successful += s + *failed += f + } + +} + +func init() { + exists, err := settings.CheckSettingsFile() + if err != nil { + logger.LogError(true, err) + } + if exists { + logger.LogInfo("Found settings file") + return + } + + // settings file does not exist, so create it and exit + settings.CreateSettingsFile() + os.Exit(0) +} + +func main() { + startingTime := time.Now().UTC() + + settings := settings.GetSettings() + + // process the given settings + if settings.CreateBlackBGImage { + err := createBlackBG(1920, 1080) + if err == nil { + logger.LogInfo("Successfully created black background") + } else { + logger.LogWarning(fmt.Sprintf("Could not create black background : %s; continuing to run...", err)) + } + } + + osuSongsDir, err := manager.GetSongsDir(settings.OsuDir) + if err != nil { + logger.LogError(true, err) + } + + if settings.ReplacementImagePath == "" || settings.ReplacementImagePath == " " { + logger.LogError(true, "Image path not specified ! Specify `pathToimage` in settings file !") + } + + // reading contents of `Songs` folder + osuSongsDirContents, err := os.ReadDir(osuSongsDir) + if err != nil { + logger.LogError(true, fmt.Sprintf("Error reading osu songs directory : %s", err.Error())) + } + + // storing all paths to each beatmap + songPaths := make(chan string, len(osuSongsDirContents)) + for _, songDir := range osuSongsDirContents { + if songDir.IsDir() { + songPaths <- filepath.Join(osuSongsDir, songDir.Name()) + } + } + logger.LogInfo(fmt.Sprintf("Found %d song folders", len(songPaths))) + + // check if there is less job than workers + if settings.Workers > len(songPaths) { + settings.Workers = len(songPaths) + } + + // replacing backgrounds for each beatmap concurrently + var successful, failed uint64 = 0, 0 + for i := 0; i < int(settings.Workers); i++ { + WG.Add(1) + go worker(songPaths, settings.ReplacementImagePath, &successful, &failed, &WG) + } + + close(songPaths) + WG.Wait() + + endTime := time.Now().UTC() + + logger.LogInfo(fmt.Sprintf("\n\nDONE in %v . %d successful; %d failed", endTime.Sub(startingTime), successful, failed)) + +} diff --git a/README.md b/README.md index f03253a..6f9dc53 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,15 @@ There is no way to return removed original backgrounds unless you delete all bea ## Installation ### From source -1. git clone [https://github.com/Unbewohnte/OBM.git](https://github.com/Unbewohnte/OBM.git) or download and unzip the archive +1. `git clone https://github.com/Unbewohnte/OBM.git` or download and unzip the archive 2. cd into the directory -3. go build +3. `go build` ### From release 1. go to the [releases](https://github.com/Unbewohnte/OBM/releases) page 2. choose your OS and download the archive 3. cd to the location of the downloaded version -4. unzip (7z x **archive_name**) - for 7z archives +4. unzip (`7z x **archive_name**`) - for 7z archives --- diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6826c34 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/Unbewohnte/OBM + +go 1.16 diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..d44cb33 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,53 @@ +package logger + +import ( + "log" + "os" + "path/filepath" +) + +var ( + infoLogger *log.Logger + warningLogger *log.Logger + errorLogger *log.Logger +) + +// creates directory for logs and sets output to file +func createLogsfile() *os.File { + logsDir := filepath.Join(".", "logs") + err := os.MkdirAll(logsDir, os.ModePerm) + if err != nil { + panic(err) + } + logfile, err := os.Create(filepath.Join(logsDir, "logs.log")) + log.SetOutput(logfile) + + return logfile +} + +// creates new custom loggers +func setUpLoggers(logfile *os.File) { + infoLogger = log.New(logfile, "INFO: ", log.Ldate|log.Ltime) + warningLogger = log.New(logfile, "WARNING: ", log.Ldate|log.Ltime) + errorLogger = log.New(logfile, "ERROR: ", log.Ldate|log.Ltime) +} + +func init() { + logfile := createLogsfile() + setUpLoggers(logfile) +} + +func LogInfo(message ...interface{}) { + infoLogger.Println(message...) +} + +func LogWarning(message ...interface{}) { + warningLogger.Println(message...) +} + +func LogError(isFatal bool, message ...interface{}) { + if isFatal { + errorLogger.Fatal(message...) + } + errorLogger.Println(message...) +} diff --git a/manager/helpers.go b/manager/helpers.go new file mode 100644 index 0000000..170751d --- /dev/null +++ b/manager/helpers.go @@ -0,0 +1,53 @@ +package manager + +import ( + "errors" + "fmt" + "io" + "os" + "strings" +) + +// checks if given string contains ".osu" file extention +func isBeatmap(filename string) bool { + if len(filename) < 5 { + return false + } + if filename[len(filename)-4:] == ".osu" { + return true + } + return false +} + +// checks if given string contains the image file extention +func isImage(filename string) bool { + var imageExtentions []string = []string{"jpeg", "jpg", "png", "JPEG", "JPG", "PNG"} + for _, extention := range imageExtentions { + if strings.Contains(filename, extention) { + return true + } + } + return false +} + +// opens given files, copies one into another +func copyFile(src, dst string) error { + srcFile, err := os.Open(src) + if err != nil { + return errors.New(fmt.Sprintf("Could not open src file : %s", err)) + } + defer srcFile.Close() + + dstFile, err := os.OpenFile(dst, os.O_WRONLY, os.ModePerm) + if err != nil { + return errors.New(fmt.Sprintf("Could not open dst file : %s", err)) + } + defer dstFile.Close() + + _, err = io.Copy(dstFile, srcFile) + if err != nil { + return errors.New(fmt.Sprintf("Could not copy file : %s", err)) + } + + return nil +} diff --git a/manager/manager.go b/manager/manager.go new file mode 100644 index 0000000..d66d830 --- /dev/null +++ b/manager/manager.go @@ -0,0 +1,113 @@ +package manager + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/Unbewohnte/OBM/logger" +) + +// filepath.Joins the main osu directory with its songs folder +func GetSongsDir(osudir string) (string, error) { + songsDir := filepath.Join(osudir, "Songs") + + stat, err := os.Stat(songsDir) + if err != nil { + return "", errors.New(fmt.Sprintf("Could not read the given path : %s", err)) + } + if !stat.IsDir() { + return "", errors.New("Given Osu! directory is not a directory !") + } + + return songsDir, nil +} + +// parses given .osu file and returns the filename of its background +func GetBackgroundName(pathToOSUbeatmap string) (string, error) { + beatmapBytes, err := os.ReadFile(pathToOSUbeatmap) + if err != nil { + return "", err + } + beatmapContents := string(beatmapBytes) + + // get index of "[Events]" (this is where BG filename is stored) + eventsIndex := strings.Index(beatmapContents, "[Events]") + if eventsIndex == -1 { + return "", errors.New("Could not retrieve index of \"[Events]\"") + } + // get index of [TimingPoints] (this tag is right after the previous "[Events]" tag, + // so we can grab the whole "[Events]" tag contents) + timingPointsIndex := strings.Index(beatmapContents, "[TimingPoints]") + if timingPointsIndex == -1 { + return "", errors.New("Could not retrieve index of \"[TimingPoints]\"") + } + contentBetween := strings.Split(beatmapContents[eventsIndex:timingPointsIndex], ",") + + for _, chunk := range contentBetween { + if isImage(chunk) { + return strings.Split(chunk, "\"")[1], nil + } + } + return "", nil +} + +// reads contents of given dir; searches for .osu files; parses them for background info; +// removes original background and replaces it with copied version of given image +func ReplaceBackgrounds(beatmapFolder, replacementPicPath string) (successful, failed uint64) { + files, err := os.ReadDir(beatmapFolder) + if err != nil { + logger.LogError(false, fmt.Sprintf("Wrong path : %s", err)) + } + for _, file := range files { + filename := file.Name() + + if isBeatmap(filename) { + beatmap := filename + + // getting BG filename + beatmapBackgroundFilename, err := GetBackgroundName(filepath.Join(beatmapFolder, beatmap)) + if err != nil { + logger.LogWarning(fmt.Sprintf("BEATMAP: %s: Error getting background filename: %s", beatmap, err)) + failed++ + continue + } + if beatmapBackgroundFilename == "" { + logger.LogWarning(fmt.Sprintf("BEATMAP: %s Could not find background filename in this beatmap file", beatmap)) + failed++ + continue + } + + backgroundPath := filepath.Join(beatmapFolder, beatmapBackgroundFilename) + + // remove old background + err = os.Remove(backgroundPath) + if err != nil { + failed++ + logger.LogWarning(fmt.Sprintf("BEATMAP: %s: Could not remove old background : %s", beatmap, err)) + } + + // create new background file + bgFile, err := os.Create(backgroundPath) + if err != nil { + failed++ + logger.LogWarning(fmt.Sprintf("BEATMAP: %s: Could not create new background file : %s", beatmap, err)) + continue + } + defer bgFile.Close() + + // copy the contents of a given image to the newly created bg file + err = copyFile(replacementPicPath, backgroundPath) + if err != nil { + logger.LogWarning(fmt.Sprintf("BEATMAP: %s: Could not copy file: %s", beatmap, err)) + failed++ + continue + } + successful++ + } + + } + return successful, failed +} diff --git a/settings/settings.go b/settings/settings.go new file mode 100644 index 0000000..3135d1a --- /dev/null +++ b/settings/settings.go @@ -0,0 +1,95 @@ +package settings + +import ( + "encoding/json" + "errors" + "fmt" + "os" + + "github.com/Unbewohnte/OBM/logger" +) + +// struct for json settings` file contents +type Settings struct { + OsuDir string `json:"pathToOsu"` + ReplacementImagePath string `json:"pathToimage"` + CreateBlackBGImage bool `json:"createBlackBackgoundImage"` + Workers int `json:"Workers"` +} + +const ( + settingsFilename string = "settings.json" +) + +var ( + defaultSettings Settings = Settings{ + OsuDir: "", + ReplacementImagePath: "", + CreateBlackBGImage: true, + Workers: 100, + } +) + +// checks if the settings.json exists in current directory +func CheckSettingsFile() (bool, error) { + files, err := os.ReadDir(".") + if err != nil { + return false, errors.New(fmt.Sprintf("ERROR : Unable to read current directory %s", err)) + } + + for _, file := range files { + if !file.IsDir() && file.Name() == settingsFilename { + return true, nil + } + } + + return false, nil +} + +// creates "settings.json" and sets the flag +func CreateSettingsFile() error { + exists, err := CheckSettingsFile() + if err != nil { + return err + } + if exists { + return nil + } + + file, err := os.Create(settingsFilename) + if err != nil { + return errors.New(fmt.Sprintf("ERROR: Error creating settings file... : %s", err)) + } + + settingsJson, err := json.MarshalIndent(defaultSettings, "", " ") + if err != nil { + return errors.New(fmt.Sprintf("ERROR: Error creating settings file... : %s", err)) + } + file.Write(settingsJson) + + file.Close() + logger.LogInfo("Successfully created new settingsFile") + + return nil +} + +// unmarshalls settings.json into struct +func GetSettings() Settings { + settingsFile, err := os.ReadFile(settingsFilename) + if err != nil { + logger.LogError(true, "Could not read settings file : ", err.Error()) + } + + var settings Settings + err = json.Unmarshal(settingsFile, &settings) + if err != nil { + logger.LogError(true, "Could not unmarshal json file : ", err) + } + + if settings.Workers <= 0 { + logger.LogInfo("`Workers` is set to 0 or less. Replaced with 1") + settings.Workers = 1 + } + + return settings +}