From 1e976d7f7c807bae377f7520b0c1e9889279b793 Mon Sep 17 00:00:00 2001 From: Unbewohnte <65883674+Unbewohnte@users.noreply.github.com> Date: Fri, 30 Apr 2021 17:18:22 +0300 Subject: [PATCH] Add files via upload --- OBM.go | 90 ++++++++++------------------------ README.md | 5 +- manager/beatmap.go | 38 +++++++++++++++ manager/paths.go | 45 +++++++++++++++++ manager/replacer.go | 71 +++++++++++++++++++++++++++ manager/retriever.go | 73 +++++++++++++++++++++++++++ settings/settings.go | 111 ++++++++++++++++++++++++++++++++++++++++++ settings/structure.go | 24 +++++++++ util/background.go | 36 ++++++++++++++ util/checks.go | 46 +++++++++++++++++ util/copy.go | 30 ++++++++++++ worker.go | 44 +++++++++++++++++ 12 files changed, 548 insertions(+), 65 deletions(-) create mode 100644 manager/beatmap.go create mode 100644 manager/paths.go create mode 100644 manager/replacer.go create mode 100644 manager/retriever.go create mode 100644 settings/settings.go create mode 100644 settings/structure.go create mode 100644 util/background.go create mode 100644 util/checks.go create mode 100644 util/copy.go create mode 100644 worker.go diff --git a/OBM.go b/OBM.go index 71de084..8d0e2bb 100644 --- a/OBM.go +++ b/OBM.go @@ -3,13 +3,13 @@ package main import ( "fmt" "os" - "path/filepath" "sync" "time" "github.com/Unbewohnte/OBM/logger" "github.com/Unbewohnte/OBM/manager" "github.com/Unbewohnte/OBM/settings" + "github.com/Unbewohnte/OBM/util" ) var ( @@ -17,8 +17,9 @@ var ( ) type job struct { - songPath string - pathToImage string + beatmapFolderPath string + replacementImagePath string + retrievementPath string } type result struct { @@ -26,55 +27,30 @@ type result struct { failed uint64 } -// a basic implementation of a concurrent worker -func worker(jobs <-chan job, results chan result, WG *sync.WaitGroup) { - defer WG.Done() - for job := range jobs { - s, f := manager.ReplaceBackgrounds(job.songPath, job.pathToImage) - results <- result{ - successful: s, - failed: f, - } - } - -} - -func workerPool(jobs chan job, results chan result, numOfWorkers int, WG *sync.WaitGroup) { - // check if there are less jobs than workers - if numOfWorkers > len(jobs) { - numOfWorkers = len(jobs) - } - - // replacing backgrounds for each beatmap concurrently - for i := 0; i < numOfWorkers; i++ { - WG.Add(1) - go worker(jobs, results, WG) - } -} - func init() { - exists, err := settings.CheckSettingsFile() + exists, err := settings.DoesExist() if err != nil { logger.LogError(true, err) } - if exists { - logger.LogInfo("Found settings file") - return + if !exists { + // settings file does not exist, so create it and exit (assuming that this is the first run) + settings.Create() + logger.LogInfo("Successfully created new settings file") + os.Exit(0) } - // settings file does not exist, so create it and exit (assuming that this is the first run) - settings.CreateSettingsFile() - os.Exit(0) + logger.LogInfo("Found settings file") + return } func main() { startingTime := time.Now() - settings := settings.GetSettings() + SETTINGS := settings.Get() - // processing given settings - if settings.CreateBlackBGImage { - err := manager.CreateBlackBG(1920, 1080) + // creating black image + if SETTINGS.CreateBlackBGImage { + err := util.CreateBlackBG(1920, 1080) if err == nil { logger.LogInfo("Successfully created black background") } else { @@ -82,37 +58,25 @@ func main() { } } - 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) + beatmaps, err := manager.GetBeatmapFolderPaths(SETTINGS.OsuDir) if err != nil { - logger.LogError(true, fmt.Sprintf("Error reading osu songs directory : %s", err.Error())) + logger.LogError(true, "Error getting beatmap folders: ", err) } + logger.LogInfo(fmt.Sprintf("Found %d beatmap folders", len(beatmaps))) // creating jobs for workers - jobs := make(chan job, len(osuSongsDirContents)) - for _, songDir := range osuSongsDirContents { - if songDir.IsDir() { - jobs <- job{ - songPath: filepath.Join(osuSongsDir, songDir.Name()), - pathToImage: settings.ReplacementImagePath, - } + jobs := make(chan job, len(beatmaps)) + for _, beatmap := range beatmaps { + jobs <- job{ + beatmapFolderPath: beatmap, + replacementImagePath: SETTINGS.BackgroundReplacement.ReplacementImagePath, + retrievementPath: SETTINGS.BackgroundRetrievement.RetrievementPath, } } close(jobs) - logger.LogInfo(fmt.Sprintf("Found %d song folders", len(jobs))) results := make(chan result, len(jobs)) - workerPool(jobs, results, settings.Workers, &WG) - + workerPool(jobs, results, SETTINGS.Workers, &WG) WG.Wait() close(results) @@ -124,7 +88,7 @@ func main() { } total := successful + failed - logger.LogInfo(fmt.Sprintf("DONE in %v. %d successful (%d%%/100%%); %d failed (%d%%/100%%)", + logger.LogInfo(fmt.Sprintf("DONE in %v. %d operations successful (%d%%/100%%); %d failed (%d%%/100%%)", time.Since(startingTime), successful, successful/total*100, failed, failed/total*100)) } diff --git a/README.md b/README.md index 34fa3a6..6705ba8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # OBM (Osu!-background-manager) -## This utility will help you with replacement of Osu!`s beatmap backgrounds (and more in the future) +## This utility will help you with replacement and retrieving Osu!`s beatmap backgrounds and more in the future **Use at your own risk !** There is no way to return removed original backgrounds unless you delete all beatmaps and reimport newly downloaded versions of them again. @@ -26,7 +26,8 @@ There is no way to return removed original backgrounds unless you delete all bea ### First run 1. The program will generate a settings.json file if it is not already in the directory when you run it -2. Paste your Osu! filepath in the "pathToOsu" field +2. Paste your Osu! filepath in the "pathToOsu" field (this field is required) +3. enable/disable 3. Paste the filepath to the image in the "pathToimage" field. **ALL** beatmap`s backgrounds will be replaced with this image 4. Additionally you can disable the "createBlackBackgoundImage" by replacing **true** with **false** or change the number of workers 5. Run the program once again diff --git a/manager/beatmap.go b/manager/beatmap.go new file mode 100644 index 0000000..54e91b6 --- /dev/null +++ b/manager/beatmap.go @@ -0,0 +1,38 @@ +package manager + +import ( + "errors" + "os" + "strings" + + "github.com/Unbewohnte/OBM/util" +) + +// parses given .osu file and returns the filename of its background +func GetBackgroundName(pathTobeatmap string) (string, error) { + beatmapBytes, err := os.ReadFile(pathTobeatmap) + 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 util.IsImage(chunk) { + return strings.Split(chunk, "\"")[1], nil + } + } + return "", nil +} diff --git a/manager/paths.go b/manager/paths.go new file mode 100644 index 0000000..ce02c56 --- /dev/null +++ b/manager/paths.go @@ -0,0 +1,45 @@ +package manager + +import ( + "errors" + "fmt" + "os" + "path/filepath" +) + +// filepath.Joins the main osu directory with its songs folder +func getSongsDir(baseOsuDir string) (string, error) { + songsDir := filepath.Join(baseOsuDir, "Songs") + + stat, err := os.Stat(songsDir) + if err != nil { + return "", errors.New(fmt.Sprintf("Could not process the given path : %s", err)) + } + if !stat.IsDir() { + return "", errors.New("Given Osu! directory is not a directory !") + } + + return songsDir, nil +} + +// returns an array of full filepaths to each beatmap from given base Osu! directory +func GetBeatmapFolderPaths(baseOsuDir string) ([]string, error) { + songsDir, err := getSongsDir(baseOsuDir) + if err != nil { + return nil, err + } + contents, err := os.ReadDir(songsDir) + if err != nil { + return nil, errors.New(fmt.Sprintf("Could not read a directory : %s", err)) + } + + var beatmapFolderPaths []string + for _, file := range contents { + if file.IsDir() { + path := filepath.Join(songsDir, file.Name()) + beatmapFolderPaths = append(beatmapFolderPaths, path) + } + } + + return beatmapFolderPaths, nil +} diff --git a/manager/replacer.go b/manager/replacer.go new file mode 100644 index 0000000..b945d83 --- /dev/null +++ b/manager/replacer.go @@ -0,0 +1,71 @@ +package manager + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/Unbewohnte/OBM/logger" + "github.com/Unbewohnte/OBM/util" +) + +// 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(true, fmt.Sprintf("Could not read directory : %s", err)) + } + for _, file := range files { + filename := file.Name() + + // if not a beatmap - skip + if !util.IsBeatmap(filename) { + continue + } + + 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 + } + + pathToBackground := filepath.Join(beatmapFolder, beatmapBackgroundFilename) + + // remove old background + err = os.Remove(pathToBackground) + 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(pathToBackground) + if err != nil { + failed++ + logger.LogWarning(fmt.Sprintf("BEATMAP: %s: Could not create new background file : %s", beatmap, err)) + continue + } + bgFile.Close() + + // copy the contents of a given image to the newly created bg file + err = util.CopyFile(replacementPicPath, pathToBackground) + 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/manager/retriever.go b/manager/retriever.go new file mode 100644 index 0000000..ee4b606 --- /dev/null +++ b/manager/retriever.go @@ -0,0 +1,73 @@ +package manager + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/Unbewohnte/OBM/logger" + "github.com/Unbewohnte/OBM/util" +) + +// retrieves backgrounds from given beatmap folder (same as in `ReplaceBackgrounds`) and copies them to the retrievement path +func RetrieveBackgrounds(beatmapFolder, retrievementPath string) (successful, failed uint64) { + files, err := os.ReadDir(beatmapFolder) + if err != nil { + logger.LogError(true, fmt.Sprintf("Could not read directory : %s", err)) + } + for _, file := range files { + filename := file.Name() + + // if not a beatmap - skip + if !util.IsBeatmap(filename) { + continue + } + + 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 + } + + pathToBackground := filepath.Join(beatmapFolder, beatmapBackgroundFilename) + + // creating a directory with the name of current beatmap folder in the retrievement path + dstPath := filepath.Join(retrievementPath, filepath.Base(beatmapFolder)) + + err = os.MkdirAll(dstPath, os.ModePerm) + if err != nil { + logger.LogWarning(fmt.Sprintf("BEATMAP: %s: Could not create a directory (%s) for copying", beatmap, dstPath)) + continue + } + + // creating a copy file + fullPathToCopy := filepath.Join(dstPath, beatmapBackgroundFilename) + dstFile, err := os.Create(fullPathToCopy) + if err != nil { + logger.LogWarning(fmt.Sprintf("BEATMAP: %s: Could not create a copy file", beatmap)) + failed++ + continue + } + dstFile.Close() + + // copy the background file to the retrievement path + err = util.CopyFile(pathToBackground, fullPathToCopy) + 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..e3fcafb --- /dev/null +++ b/settings/settings.go @@ -0,0 +1,111 @@ +package settings + +import ( + "encoding/json" + "errors" + "fmt" + "os" + + "github.com/Unbewohnte/OBM/logger" + "github.com/Unbewohnte/OBM/util" +) + +const ( + settingsFilename string = "settings.json" +) + +// checks if the settings.json exists in current directory +func DoesExist() (bool, error) { + files, err := os.ReadDir(".") + if err != nil { + return false, errors.New(fmt.Sprintf("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 Create() error { + exists, err := DoesExist() + if err != nil { + return err + } + if exists { + return nil + } + + file, err := os.Create(settingsFilename) + if err != nil { + return errors.New(fmt.Sprintf("Unable to create settings file : %s", err)) + } + + // marshaling default settings + settingsJson, err := json.MarshalIndent(Settings{ + OsuDir: "", + BackgroundReplacement: backgroundReplacement{ + Enabled: true, + ReplacementImagePath: "", + }, + BackgroundRetrievement: backgroundRetrievement{ + Enabled: false, + RetrievementPath: "", + }, + CreateBlackBGImage: true, + Workers: 100, + }, "", " ") + if err != nil { + return errors.New(fmt.Sprintf("Could not marshal settings into file : %s", err)) + } + file.Write(settingsJson) + + file.Close() + + return nil +} + +// unmarshalls settings.json into struct +func Get() Settings { + settingsFileContents, err := os.ReadFile(settingsFilename) + if err != nil { + logger.LogError(true, fmt.Sprintf("Could not read settings file : %s", err)) + } + + var settings Settings + err = json.Unmarshal(settingsFileContents, &settings) + if err != nil { + logger.LogError(true, fmt.Sprintf("Could not unmarshal json file : %s", err)) + } + + // checking for edge cases or mistakes made in the settings file, + // enabled and disabled fields + if settings.BackgroundReplacement.Enabled { + if settings.BackgroundReplacement.ReplacementImagePath == "" || settings.BackgroundReplacement.ReplacementImagePath == " " { + logger.LogError(true, "`replacementImagePath` is not specified !") + } else if !util.IsImage(settings.BackgroundReplacement.ReplacementImagePath) { + logger.LogError(true, "`replacementImagePath` is pointing to a non-image file !`") + } + } else { + settings.BackgroundReplacement.ReplacementImagePath = "" + } + + if settings.BackgroundRetrievement.Enabled { + if settings.BackgroundRetrievement.RetrievementPath == "" || settings.BackgroundRetrievement.RetrievementPath == " " { + logger.LogError(true, "`retrievementPath` is not specified !") + } + } else { + settings.BackgroundReplacement.ReplacementImagePath = "" + } + + if settings.Workers <= 0 { + settings.Workers = 1 + logger.LogWarning("`workers` is set to 0 or less. Replaced with 1") + } + + return settings +} diff --git a/settings/structure.go b/settings/structure.go new file mode 100644 index 0000000..df5972b --- /dev/null +++ b/settings/structure.go @@ -0,0 +1,24 @@ +package settings + +// the idea behind `Enabled` field is that if it`s not true - then +// we treat the path below as "" (blank string) , which workers will just ignore +// (therefore will not perform the replacement or retrievement) + +type backgroundReplacement struct { + Enabled bool `json:"enabled"` + ReplacementImagePath string `json:"pathToimage"` +} + +type backgroundRetrievement struct { + Enabled bool `json:"enabled"` + RetrievementPath string `json:"retrievementPath"` +} + +// struct for json settings` file contents +type Settings struct { + OsuDir string `json:"pathToOsu"` + BackgroundReplacement backgroundReplacement `json:"backgroundReplacement"` + BackgroundRetrievement backgroundRetrievement `json:"backgroundRetrievement"` + CreateBlackBGImage bool `json:"createBlackBackgoundImage"` + Workers int `json:"workers"` +} diff --git a/util/background.go b/util/background.go new file mode 100644 index 0000000..4296d4b --- /dev/null +++ b/util/background.go @@ -0,0 +1,36 @@ +package util + +import ( + "errors" + "fmt" + "image" + "image/color" + "image/png" + "os" +) + +// 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 +} diff --git a/util/checks.go b/util/checks.go new file mode 100644 index 0000000..001615e --- /dev/null +++ b/util/checks.go @@ -0,0 +1,46 @@ +package util + +import ( + "os" + "strings" +) + +func IsDir(path string) bool { + info, err := os.Stat(path) + if err != nil { + // path error + return false + } + if !info.IsDir() { + return false + } + return true +} + +// checks if given string contains ".osu" file extention +func IsBeatmap(filename string) bool { + if len(filename) < 5 { + // too short filename to be a beatmap file + 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 { + if IsDir(filename) { + // given filename is actually a directory + return false + } + + var imageExtentions []string = []string{"jpeg", "jpg", "png", "JPEG", "JPG", "PNG"} + for _, extention := range imageExtentions { + if strings.Contains(filename, extention) { + return true + } + } + return false +} diff --git a/util/copy.go b/util/copy.go new file mode 100644 index 0000000..0a7be28 --- /dev/null +++ b/util/copy.go @@ -0,0 +1,30 @@ +package util + +import ( + "errors" + "fmt" + "io" + "os" +) + +// 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/worker.go b/worker.go new file mode 100644 index 0000000..81147cd --- /dev/null +++ b/worker.go @@ -0,0 +1,44 @@ +package main + +import ( + "sync" + + "github.com/Unbewohnte/OBM/manager" +) + +// a basic implementation of a concurrent worker +func worker(jobs <-chan job, results chan result, WG *sync.WaitGroup) { + defer WG.Done() + for job := range jobs { + var successful, failed uint64 = 0, 0 + + if job.retrievementPath != "" { + s, f := manager.RetrieveBackgrounds(job.beatmapFolderPath, job.retrievementPath) + successful += s + failed += f + } + if job.replacementImagePath != "" { + s, f := manager.ReplaceBackgrounds(job.beatmapFolderPath, job.replacementImagePath) + successful += s + failed += f + } + results <- result{ + successful: successful, + failed: failed, + } + } + +} + +func workerPool(jobs chan job, results chan result, numOfWorkers int, WG *sync.WaitGroup) { + // check if there are less jobs than workers + if numOfWorkers > len(jobs) { + numOfWorkers = len(jobs) + } + + // replacing backgrounds for each beatmap concurrently + for i := 0; i < numOfWorkers; i++ { + WG.Add(1) + go worker(jobs, results, WG) + } +}