Compare commits

...

31 Commits
1.2.1 ... main

Author SHA1 Message Date
Kasianov Nikolai Alekseevich 11d493bb14 Removed accidental logs file 2 years ago
Kasianov Nikolai Alekseevich 76b489b424 Tidied a bit, changed go.mod 2 years ago
Gitea 09c3956cb9 No more github 2 years ago
Unbewohnte 88f50f8724 Little fix 3 years ago
Unbewohnte e76f34e0fb
Important note 3 years ago
Unbewohnte cbf2354d1e
Add files via upload 3 years ago
Unbewohnte ed33ca27c3
Some minor documentaion and notes added 3 years ago
Unbewohnte f9eca425b6
Typo 3 years ago
Unbewohnte b91af019b4
Title 3 years ago
Unbewohnte 7bc3ed6ed0
Added forgotten default field 3 years ago
Unbewohnte 3a4684f3f1
Wrong path... 3 years ago
Unbewohnte 8eded6b6b0
Added forgotten default field 3 years ago
Unbewohnte 728beb991e
Removement, flags, settings etc... 3 years ago
Unbewohnte 2ff153c13c
Add files via upload 3 years ago
Unbewohnte be111c07c8
English 3 years ago
Unbewohnte 3716ae1079
More. Clear. Information 3 years ago
Unbewohnte 54bcd0c060
More clear explanation of "-beatmap" flag 3 years ago
Unbewohnte 7a2dbbf7ab
Added support for -beatmap flag 3 years ago
Unbewohnte 5647872b11
Add files via upload 3 years ago
Unbewohnte 587962840a
Delete paths.go 3 years ago
Unbewohnte 01a842f10c
Add files via upload 3 years ago
Unbewohnte f7e8062097
Added some new background creation fields 3 years ago
Unbewohnte 5d8af5a146
Redid the layout; improved code readability 3 years ago
Unbewohnte 75b2731c51
Delete manager directory 3 years ago
Unbewohnte 9e962dc1a4
Fixed a silly bug 3 years ago
Unbewohnte 50e4fc4bae
Fixed obvious typos 3 years ago
Unbewohnte 88b9f8075a
Add files via upload 3 years ago
Unbewohnte 1e976d7f7c
Add files via upload 3 years ago
Unbewohnte c7dc73bae3
Delete manager directory 3 years ago
Unbewohnte c7bc70cf0d
Delete settings directory 3 years ago
Unbewohnte 2c564d109d
Add files via upload 3 years ago
  1. 3
      .gitignore
  2. 2
      Makefile
  3. 130
      OBM.go
  4. 37
      README.md
  5. 3
      go.mod
  6. 82
      manager/helpers.go
  7. 113
      manager/manager.go
  8. 95
      settings/settings.go
  9. 180
      src/OBM.go
  10. 3
      src/go.mod
  11. 0
      src/logger/logger.go
  12. 88
      src/manager/beatmap.go
  13. 43
      src/manager/parse.go
  14. 31
      src/manager/remove.go
  15. 36
      src/manager/replace.go
  16. 52
      src/manager/retrieve.go
  17. 20
      src/manager/search.go
  18. 123
      src/settings/settings.go
  19. 35
      src/settings/structure.go
  20. 36
      src/util/background.go
  21. 56
      src/util/checks.go
  22. 32
      src/util/copy.go

3
.gitignore vendored

@ -0,0 +1,3 @@
settings.json
OBM
logs

2
Makefile

@ -0,0 +1,2 @@
all:
cd src && go build && mv OBM ..

130
OBM.go

@ -1,130 +0,0 @@
package main
import (
"fmt"
"os"
"path/filepath"
"sync"
"time"
"github.com/Unbewohnte/OBM/logger"
"github.com/Unbewohnte/OBM/manager"
"github.com/Unbewohnte/OBM/settings"
)
var (
WG sync.WaitGroup
)
type job struct {
songPath string
pathToImage string
}
type result struct {
successful uint64
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()
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 (assuming that this is the first run)
settings.CreateSettingsFile()
os.Exit(0)
}
func main() {
startingTime := time.Now()
settings := settings.GetSettings()
// processing given settings
if settings.CreateBlackBGImage {
err := manager.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()))
}
// 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,
}
}
}
close(jobs)
logger.LogInfo(fmt.Sprintf("Found %d song folders", len(jobs)))
results := make(chan result, len(jobs))
workerPool(jobs, results, settings.Workers, &WG)
WG.Wait()
close(results)
// extracting results and logging the last message
var successful, failed uint64 = 0, 0
for result := range results {
successful += result.successful
failed += result.failed
}
total := successful + failed
logger.LogInfo(fmt.Sprintf("DONE in %v. %d successful (%d%%/100%%); %d failed (%d%%/100%%)",
time.Since(startingTime), successful, successful/total*100, failed, failed/total*100))
}

37
README.md

@ -1,6 +1,7 @@
# OBM (Osu!-background-manager)
# 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, retrievement and removement of Osu!\`s beatmaps\` backgrounds
## NOTE: does NOT work with Osu!Lazer (Lazer has a different handling for beatmaps)
**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.
@ -9,31 +10,51 @@ There is no way to return removed original backgrounds unless you delete all bea
## Installation
### From source
### From source (You`ll need [Go](https://golang.org/dl/) installed)
1. `git clone https://github.com/Unbewohnte/OBM.git` or download and unzip the archive
2. cd into the directory
2. `cd` into the directory
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
3. `cd` to the location of the downloaded version
4. unzip (`7z x **archive_name**`) - for 7z archives
---
## Usage
To run - `./OBM` in terminal (on Unix) || `OBM.exe` in command line (on Windows) (a simple double-click on exe will certainly work as well)
### 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
3. Paste the filepath to the image in the "pathToimage" field. **ALL** beatmap`s backgrounds will be replaced with this image
3. Enable/Disable needed features, providing valid filepaths to them
4. Additionally you can disable the "createBlackBackgoundImage" by replacing **true** with **false** or change the number of workers
5. Run the program once again
### After
1. Just run the utility again. If it found the settings file - it will perform the magic
1. Start the utility again. If it has found the settings file - it will perform the magic according to provided rules
### Flags
Right now there are 2 arguments that you can specify before running the program - "beatmap" and "showOrder".
"-beatmap" flag takes a string; it will tell the program to do its work **ONLY** on beatmaps with specified name; others will be ignored.
The names of beatmaps in Osu! consist an id, artist and the name of the soundtrack, so you can
specify any name in the flag that will contain one of those parts.
"-showOrder" flag takes a boolean; if set to **true** - it will print an order in which the workers perform operations over each beatmap. Right now it`s just a helper flag.
#### Examples
1. `./OBM -beatmap=""` - the same as just `./OBM`. It will affect **all** of your beatmaps
2. `./OBM -beatmap="Demetori"` - this will search for beatmaps with names that contain "Demetori" and will work only with them
3. `./OBM -beatmap=Demetori` - the same as before, but without "" (look at 4 - 5 for nuances)
4. `./OBM -beatmap=raise my sword` - NOTE that this will make the program look only for word "raise", but not for the whole sequence
5. `./OBM -beatmap="raise my sword"` - this is the valid option for 4 (You need to use "" in case of a multi-word name)
The search is case-insensitive, so for example `./OBM -beatmap="Road of Resistance"` and `./OBM -beatmap="ROAD of rEsIsTaNcE"` will get you the same results
6. `./OBM -showOrder=true` - will print the order and exit
7. `./OBM -showOrder=true -beatmap="something here"` - will print the order and exit, just like in the previous one
---
## License

3
go.mod

@ -1,3 +0,0 @@
module github.com/Unbewohnte/OBM
go 1.16

82
manager/helpers.go

@ -1,82 +0,0 @@
package manager
import (
"errors"
"fmt"
"image"
"image/color"
"image/png"
"io"
"os"
"strings"
)
// 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
}
// checks if given string contains ".osu" file extention (NOT EXPORTED !)
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 (NOT EXPORTED !)
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 (NOT EXPORTED !)
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
}

113
manager/manager.go

@ -1,113 +0,0 @@
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
}

95
settings/settings.go

@ -1,95 +0,0 @@
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
}

180
src/OBM.go

@ -0,0 +1,180 @@
package main
import (
"flag"
"fmt"
"os"
"sync"
"time"
"unbewohnte/OBM/logger"
"unbewohnte/OBM/manager"
"unbewohnte/OBM/settings"
"unbewohnte/OBM/util"
)
const orderMessage string = "Order: Replace -> Retrieve -> Remove\n"
var (
cmdlnBeatmap = flag.String("beatmap", "", `Specifies a certain beatmap.
If set to non "" - the program will search for given name and perform the magic
provided in settings if successful`)
showOrder = flag.Bool("showOrder", false, "Prints an order in which functions are performed on a beatmap")
)
// 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 uint = 0, 0
// the order is: Replace->Retrieve->Remove (if all 3 options are enabled)
if job.replacementImagePath != "" {
s, f := job.beatmap.ReplaceBackgrounds(job.replacementImagePath)
successful += s
failed += f
}
if job.retrievementPath != "" {
s, f := job.beatmap.RetrieveBackgrounds(job.retrievementPath)
successful += s
failed += f
}
if job.remove == true {
s, f := job.beatmap.RemoveBackgrounds()
successful += s
failed += f
}
results <- result{
beatmapName: job.beatmap.Name,
numberOfDiffs: uint(len(job.beatmap.Diffs)),
successful: successful,
failed: failed,
}
}
}
// the `starter` that `glues` workers and jobs together
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)
}
}
type result struct {
beatmapName string
numberOfDiffs uint
successful uint
failed uint
}
type job struct {
beatmap manager.Beatmap
replacementImagePath string
retrievementPath string
remove bool
}
func init() {
exists, err := settings.DoesExist()
if err != nil {
logger.LogError(true, err)
}
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)
}
logger.LogInfo("Found settings file")
// parse for `-beatmap` argument
flag.Parse()
// if `-showOrder` is checked - show the message and exit
if *showOrder == true {
fmt.Print(orderMessage)
os.Exit(0)
}
return
}
func main() {
var WG sync.WaitGroup
startingTime := time.Now()
SETTINGS := settings.Get()
// creating black image if enabled
if SETTINGS.CreateBlackBGImage.Enabled {
err := util.CreateBlackBG(SETTINGS.CreateBlackBGImage.Width, SETTINGS.CreateBlackBGImage.Height)
if err == nil {
logger.LogInfo("Successfully created black background")
} else {
logger.LogWarning(fmt.Sprintf("Could not create black background : %s; continuing to run...", err))
}
}
// get an array of all beatmaps
beatmaps, err := manager.GetBeatmaps(SETTINGS.OsuDir)
if err != nil {
logger.LogError(true, "Error getting beatmaps: ", err)
}
logger.LogInfo(fmt.Sprintf("Found %d beatmaps", len(beatmaps)))
// If `-beatmap` flag is specified - do the magic only on found beatmaps
if *cmdlnBeatmap != "" {
logger.LogInfo(fmt.Sprintf("Trying to locate \"%s\"...", *cmdlnBeatmap))
found, n := manager.Search(beatmaps, *cmdlnBeatmap)
logger.LogInfo(fmt.Sprintf("Checked %d beatmaps. Found %d instance(s)", n, len(found)))
// if found nothing - exit
if len(found) == 0 {
os.Exit(0)
}
// replace all beatmaps with found ones
beatmaps = found
}
// creating jobs for workers
jobs := make(chan job, len(beatmaps))
for _, beatmap := range beatmaps {
jobs <- job{
beatmap: beatmap,
replacementImagePath: SETTINGS.BackgroundReplacement.ReplacementImagePath,
retrievementPath: SETTINGS.BackgroundRetrievement.RetrievementPath,
remove: SETTINGS.BackgroundRemovement.Enabled,
}
}
close(jobs)
// perform the magic
results := make(chan result, len(jobs))
workerPool(jobs, results, SETTINGS.Workers, &WG)
WG.Wait()
close(results)
// extracting results and logging the last message
var successful, failed uint = 0, 0
for result := range results {
successful += result.successful
failed += result.failed
logger.LogInfo(fmt.Sprintf("Beatmap: %s; Number of diffs: %d;\n Successful: %d; Failed: %d",
result.beatmapName, result.numberOfDiffs, result.successful, result.failed))
}
total := successful + failed
logger.LogInfo(fmt.Sprintf("DONE in %v. %d operations successful (%.2f%%/100%%); %d failed (%.2f%%/100%%)",
time.Since(startingTime), successful, float32(successful)/float32(total)*100, failed, float32(failed)/float32(total)*100))
}

3
src/go.mod

@ -0,0 +1,3 @@
module unbewohnte/OBM
go 1.16

0
logger/logger.go → src/logger/logger.go

88
src/manager/beatmap.go

@ -0,0 +1,88 @@
package manager
import (
"fmt"
"os"
"path/filepath"
"unbewohnte/OBM/util"
)
// the main beatmap struct, contains necessary data for functions
type Beatmap struct {
Name string
Path string
Diffs []string
}
// 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 "", fmt.Errorf("could not process the given path : %s", err)
}
if !stat.IsDir() {
return "", fmt.Errorf("given Osu! directory is not a directory")
}
return songsDir, nil
}
// checks for .osu files in given path and returns all found instances
func getDiffs(path string) ([]string, error) {
files, err := os.ReadDir(path)
if err != nil {
return nil, fmt.Errorf("could not read a directory : %s", err)
}
var diffs []string
for _, file := range files {
filename := file.Name()
if util.IsBeatmap(filename) {
diffs = append(diffs, filename)
}
}
return diffs, nil
}
// constructs a `Beatmap` struct and returns it
func newBeatmap(name, path string, diffs []string) Beatmap {
return Beatmap{
Name: name,
Path: path,
Diffs: diffs,
}
}
// returns an array of beatmaps from given base Osu! directory
func GetBeatmaps(baseOsuDir string) ([]Beatmap, error) {
songsDir, err := getSongsDir(baseOsuDir)
if err != nil {
return nil, err
}
contents, err := os.ReadDir(songsDir)
if err != nil {
return nil, fmt.Errorf("could not read a directory : %s", err)
}
var beatmaps []Beatmap
// looping through all folders in yourOsuDir/Songs/ directory
for _, file := range contents {
if file.IsDir() {
// retrieving all necessary data for creating a new instance of a beatmap
beatmapName := file.Name()
pathToBeatmap := filepath.Join(songsDir, beatmapName)
diffs, err := getDiffs(pathToBeatmap)
if err != nil {
continue
}
newBeatmap := newBeatmap(beatmapName, pathToBeatmap, diffs)
beatmaps = append(beatmaps, newBeatmap)
}
}
return beatmaps, nil
}

43
src/manager/parse.go

@ -0,0 +1,43 @@
package manager
import (
"fmt"
"os"
"path/filepath"
"strings"
"unbewohnte/OBM/util"
)
// parses given .osu file and returns the filename of its background
// NOTE: Osu! beatmap (as whole) can have multiple backgrounds for each .osu file
// the perfect example : https://osu.ppy.sh/beatmapsets/43701#osu/137122
// this is why this function asks for a certain difficulty (.osu filename) to be sure
// to return the correct background name
func (BEATMAP *Beatmap) GetBackgroundName(diff string) (string, error) {
beatmapBytes, err := os.ReadFile(filepath.Join(BEATMAP.Path, diff))
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 "", fmt.Errorf("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 "", fmt.Errorf("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
}

31
src/manager/remove.go

@ -0,0 +1,31 @@
package manager
import (
"fmt"
"os"
"path/filepath"
"unbewohnte/OBM/logger"
)
// parses each difficulty for background info, removes found backgrounds
func (BEATMAP *Beatmap) RemoveBackgrounds() (successful, failed uint) {
// looping through each .osu file of a beatmap
for _, diff := range BEATMAP.Diffs {
background, err := BEATMAP.GetBackgroundName(diff)
if err != nil || background == "" {
logger.LogError(false, fmt.Sprintf("BEATMAP: %s: Error getting background filename: %s", diff, err))
failed++
continue
}
// remove background
err = os.Remove(filepath.Join(BEATMAP.Path, background))
if err != nil {
// background file does not exist (success ???)
successful++
continue
}
successful++
}
return successful, failed
}

36
src/manager/replace.go

@ -0,0 +1,36 @@
package manager
import (
"fmt"
"os"
"path/filepath"
"unbewohnte/OBM/logger"
"unbewohnte/OBM/util"
)
// parses each beatmap`s .osu file for background info;
// removes original background and replaces it with copied version of given image
func (BEATMAP *Beatmap) ReplaceBackgrounds(replacementImgPath string) (successful, failed uint) {
// looping through each .osu file of a beatmap
for _, diff := range BEATMAP.Diffs {
background, err := BEATMAP.GetBackgroundName(diff)
if err != nil || background == "" {
logger.LogError(false, fmt.Sprintf("BEATMAP: %s: Error getting background filename: %s", diff, err))
failed++
continue
}
// remove old bg (if there is no background file - no need to worry)
os.Remove(filepath.Join(BEATMAP.Path, background))
// copy given picture, thus replacing background
err = util.CopyFile(replacementImgPath, filepath.Join(BEATMAP.Path, background))
if err != nil {
logger.LogError(false, fmt.Sprintf("BEATMAP: %s: Could not copy: %s", diff, err))
failed++
continue
}
successful++
}
return successful, failed
}

52
src/manager/retrieve.go

@ -0,0 +1,52 @@
package manager
import (
"fmt"
"os"
"path/filepath"
"unbewohnte/OBM/logger"
"unbewohnte/OBM/util"
)
// retrieves backgrounds from given beatmap folder (same as in `ReplaceBackgrounds`) and copies them to the retrievement path
func (BEATMAP *Beatmap) RetrieveBackgrounds(retrievementPath string) (successful, failed uint) {
// looping through each .osu file of a beatmap
for _, diff := range BEATMAP.Diffs {
background, err := BEATMAP.GetBackgroundName(diff)
if err != nil || background == "" {
logger.LogError(false, fmt.Sprintf("BEATMAP: %s: Error getting background filename: %s", diff, err))
failed++
continue
}
// check if the background does exist
exists := util.DoesExist(filepath.Join(BEATMAP.Path, background))
if !exists {
// if not - we cannot copy it, so moving to the next diff
logger.LogWarning(fmt.Sprintf("BEATMAP: %s: Background does not exist", diff))
failed++
continue
}
// creating directory that represents current beatmap
dstPath := filepath.Join(retrievementPath, BEATMAP.Name)
err = os.MkdirAll(dstPath, os.ModePerm)
if err != nil {
logger.LogError(false, fmt.Sprintf("BEATMAP: %s: Error creating a directory: %s", diff, err))
failed++
continue
}
// copy background to the `retrievementPath`
err = util.CopyFile(filepath.Join(BEATMAP.Path, background), filepath.Join(dstPath, background))
if err != nil {
logger.LogError(false, fmt.Sprintf("BEATMAP: %s: Could not copy: %s", diff, err))
failed++
continue
}
successful++
}
return successful, failed
}

20
src/manager/search.go

@ -0,0 +1,20 @@
package manager
import "strings"
// Search tries to locate instances of beatmaps with the provided name (or part of the name);
// returns a slice of found beatmaps and a number of searched beatmaps
func Search(beatmaps []Beatmap, name string) ([]Beatmap, uint64) {
var instances []Beatmap
var searched uint64 = 0
// to make the search case-insensitive
name = strings.ToLower(name)
for _, beatmap := range beatmaps {
if strings.Contains(strings.ToLower(beatmap.Name), name) {
instances = append(instances, beatmap)
}
searched++
}
return instances, searched
}

123
src/settings/settings.go

@ -0,0 +1,123 @@
package settings
import (
"encoding/json"
"fmt"
"os"
"unbewohnte/OBM/logger"
"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, fmt.Errorf("wasn`t able 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" in current directory
func Create() error {
exists, err := DoesExist()
if err != nil {
return err
}
if exists {
return nil
}
file, err := os.Create(settingsFilename)
if err != nil {
return fmt.Errorf("could not create settings file : %s", err)
}
defer file.Close()
// marshaling default settings
settingsJson, err := json.MarshalIndent(Settings{
OsuDir: "",
BackgroundReplacement: backgroundReplacement{
Enabled: true,
ReplacementImagePath: "",
},
BackgroundRetrievement: backgroundRetrievement{
Enabled: false,
RetrievementPath: "",
},
BackgroundRemovement: backgroundRemovement{
Enabled: false,
},
CreateBlackBGImage: backgroundCreatement{
Enabled: true,
Width: 1920,
Height: 1080,
},
Workers: 100,
}, "", " ")
if err != nil {
return fmt.Errorf("could not marshal settings into file : %s", err)
}
file.Write(settingsJson)
return nil
}
// unmarshalls settings.json into struct and processes the edge-cases
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))
}
// if all features are disabled
if !settings.BackgroundReplacement.Enabled && !settings.BackgroundRetrievement.Enabled && !settings.BackgroundRemovement.Enabled {
logger.LogInfo("No features enabled. Exiting...")
os.Exit(0)
}
// 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.BackgroundRetrievement.RetrievementPath = ""
}
if settings.Workers <= 0 {
settings.Workers = 1
logger.LogWarning("`workers` is set to 0 or less. Replaced with 1")
}
return settings
}

35
src/settings/structure.go

@ -0,0 +1,35 @@
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"`
}
type backgroundRemovement struct {
Enabled bool `json:"enabled"`
}
type backgroundCreatement struct {
Enabled bool `json:"enabled"`
Width uint `json:"width"`
Height uint `json:"height"`
}
// struct for json settings` file contents
type Settings struct {
OsuDir string `json:"pathToOsu"`
BackgroundReplacement backgroundReplacement `json:"backgroundReplacement"`
BackgroundRetrievement backgroundRetrievement `json:"backgroundRetrievement"`
BackgroundRemovement backgroundRemovement `json:"backgroundRemovement"`
CreateBlackBGImage backgroundCreatement `json:"blackBackgroundCreatement"`
Workers int `json:"workers"`
}

36
src/util/background.go

@ -0,0 +1,36 @@
package util
import (
"errors"
"fmt"
"image"
"image/color"
"image/png"
"os"
)
// creates a complete black image file in current working directory
func CreateBlackBG(width, height uint) 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, int(width), int(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
}

56
src/util/checks.go

@ -0,0 +1,56 @@
package util
import (
"os"
"strings"
)
// checks if given path is a directory
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
}
// checks if given directory/file does exist
func DoesExist(path string) bool {
_, err := os.Stat(path)
if err != nil {
return false
}
return true
}

32
src/util/copy.go

@ -0,0 +1,32 @@
package util
import (
"errors"
"fmt"
"io"
"os"
)
// opens given files, copies one into another
func CopyFile(srcPath, dstPath string) error {
// open for reading (source)
srcFile, err := os.Open(srcPath)
if err != nil {
return errors.New(fmt.Sprintf("Could not open src file : %s", err))
}
defer srcFile.Close()
// open for writing (destination) (create file, if it does not exist already)
dstFile, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE, 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
}
Loading…
Cancel
Save