the start of the go backend

This commit is contained in:
2025-02-02 03:54:03 +01:00
parent bc353a90e7
commit ee7cf10b28
7 changed files with 638 additions and 0 deletions

2
go-backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
data/*
.vscode/

316
go-backend/database.go Normal file
View File

@@ -0,0 +1,316 @@
package main
import (
"database/sql"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"github.com/juli0n21/go-osu-parser/parser"
_ "modernc.org/sqlite"
)
var ErrBeatmapCountNotMatch = errors.New("beatmap count not matching")
func initDB(connectionString string, osuDb *parser.OsuDB) (*sql.DB, error) {
dir := filepath.Dir(connectionString)
if _, err := os.Stat(dir); os.IsNotExist(err) {
err = os.MkdirAll(dir, 0755)
if err != nil {
return nil, fmt.Errorf("failed to create directory %s: %v", dir, err)
}
}
db, err := sql.Open("sqlite", connectionString)
if err != nil {
return nil, fmt.Errorf("failed to open SQLite database %s: %v", connectionString, err)
}
if err = createDB(db); err != nil {
return nil, err
}
if err = checkhealth(db, osuDb); err != nil {
if err = rebuildDb(db, osuDb); err != nil {
return nil, err
}
}
return db, nil
}
func createDB(db *sql.DB) error {
_, err := db.Query(`
CREATE TABLE IF NOT EXISTS Beatmap (
BeatmapId INTEGER DEFAULT 0,
Artist TEXT DEFAULT '?????',
ArtistUnicode TEXT DEFAULT '?????',
Title TEXT DEFAULT '???????',
TitleUnicode TEXT DEFAULT '???????',
Creator TEXT DEFAULT '?????',
Difficulty TEXT DEFAULT '1',
AudioFileName TEXT DEFAULT 'unknown.mp3',
MD5Hash TEXT DEFAULT '00000000000000000000000000000000',
FileName TEXT DEFAULT 'unknown.osu',
RankedStatus TEXT DEFAULT Unknown,
LastModifiedTime DATETIME DEFAULT '0001-01-01 00:00:00',
TotalTime INTEGER DEFAULT 0,
AudioPreviewTime INTEGER DEFAULT 0,
BeatmapSetId INTEGER DEFAULT -1,
Source TEXT DEFAULT '',
Tags TEXT DEFAULT '',
LastPlayed DATETIME DEFAULT '0001-01-01 00:00:00',
FolderName TEXT DEFAULT 'Unknown Folder',
UNIQUE (Artist, Title, MD5Hash, Difficulty)
);
`)
if err != nil {
return err
}
return nil
}
func checkhealth(db *sql.DB, osuDb *parser.OsuDB) error {
rows, err := db.Query(`SELECT COUNT(*) FROM Beatmap GROUP BY BeatmapSetId;`)
if err != nil {
return err
}
defer rows.Close()
var count int
if err = rows.Scan(&count); err != nil {
return err
}
if count != int(osuDb.FolderCount) {
log.Println("Folder count missmatch rebuilding db...")
return ErrBeatmapCountNotMatch
}
rows, err = db.Query(`SELECT COUNT(*) FROM Beatmap;`)
if err != nil {
return err
}
if err = rows.Scan(&count); err != nil {
return err
}
if count != int(osuDb.NumberOfBeatmaps) {
log.Println("Beatmap count missmatch rebuilding db...")
return ErrBeatmapCountNotMatch
}
return nil
}
func rebuildDb(db *sql.DB, osuDb *parser.OsuDB) error {
if _, err := db.Query("DROP TABLE Beatmap"); err != nil {
return err
}
if err := createDB(db); err != nil {
return err
}
stmt, err := db.Prepare(`
INSERT INTO Beatmap (
BeatmapId, Artist, ArtistUnicode, Title, TitleUnicode, Creator,
Difficulty, AudioFileName, MD5Hash, FileName, RankedStatus,
LastModifiedTime, TotalTime, AudioPreviewTime, BeatmapSetId,
Source, Tags, LastPlayed, FolderName
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`)
// ON CONFLICT (Artist, Title, MD5Hash) DO NOTHING
if err != nil {
return err
}
defer stmt.Close()
tx, err := db.Begin()
if err != nil {
return err
}
stmt = tx.Stmt(stmt)
for i, beatmap := range osuDb.Beatmaps {
//fmt.Println(i, beatmap.Artist, beatmap.SongTitle, beatmap.MD5Hash)
_, err := stmt.Exec(
beatmap.DifficultyID, beatmap.Artist, beatmap.ArtistUnicode,
beatmap.SongTitle, beatmap.SongTitleUnicode, beatmap.Creator,
beatmap.Difficulty, beatmap.AudioFileName, beatmap.MD5Hash,
beatmap.FileName, beatmap.RankedStatus, beatmap.LastModificationTime,
beatmap.TotalTime, beatmap.AudioPreviewStartTime, beatmap.BeatmapID,
beatmap.SongSource, beatmap.SongTags, beatmap.LastPlayed, beatmap.FolderName,
)
if err != nil {
fmt.Println(i, "hash: ", beatmap.MD5Hash, "artist:", beatmap.Artist, "title:", beatmap.SongTitle, err)
}
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func getBeatmapCount(db *sql.DB) int {
rows, err := db.Query("SELECT COUNT(*) FROM Beatmap")
if err != nil {
log.Println(err)
return 0
}
defer rows.Close()
var count int
if rows.Next() {
err = rows.Scan(&count)
if err != nil {
log.Println(err)
return 0
}
} else {
return 0
}
return count
}
func getRecent(db *sql.DB, limit, offset int) ([]Song, error) {
rows, err := db.Query("SELECT * FROM Songs ORDER BY LastPlayed DESC LIMIT ? OFFSET ?", limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
return scanSongs(rows)
}
func getSearch(db *sql.DB, q string, limit, offset int) (ActiveSearch, error) {
rows, err := db.Query("SELECT * FROM Songs WHERE Title LIKE ? OR Artist LIKE ? LIMIT ? OFFSET ?", "%"+q+"%", "%"+q+"%", limit, offset)
if err != nil {
return ActiveSearch{}, err
}
defer rows.Close()
_, err = scanSongs(rows)
if err != nil {
return ActiveSearch{}, err
}
return ActiveSearch{}, nil
}
func getArtists(db *sql.DB, q string, limit, offset int) ([]string, error) {
rows, err := db.Query("SELECT * FROM Songs WHERE Title LIKE ? OR Artist LIKE ? LIMIT ? OFFSET ?", "%"+q+"%", "%"+q+"%", limit, offset)
if err != nil {
return []string{}, err
}
defer rows.Close()
_, err = scanSongs(rows)
if err != nil {
return []string{}, err
}
return []string{}, nil
}
func getFavorites(db *sql.DB, q string, limit, offset int) ([]Song, error) {
rows, err := db.Query("SELECT * FROM Songs WHERE IsFavorite = 1 LIMIT ? OFFSET ?", limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
return scanSongs(rows)
}
func getCollection(db *sql.DB, index int) (Collection, error) {
row := db.QueryRow("SELECT * FROM Collections WHERE CollectionId = ?", index)
return scanCollection(row)
}
func getCollections(db *sql.DB, q string, limit, offset int) ([]Collection, error) {
//not correct
rows, err := db.Query("SELECT * FROM Collections WHERE name = ? LIMIT ? OFFSET ?", q, limit, offset)
if err != nil {
return []Collection{}, err
}
return scanCollections(rows)
}
func getSong(db *sql.DB, hash string) (Song, error) {
row := db.QueryRow("SELECT * FROM Songs WHERE MD5Hash = ?", hash)
return scanSong(row)
}
func scanSongs(rows *sql.Rows) ([]Song, error) {
var songs []Song
for rows.Next() {
var s Song
if err := rows.Scan(&s); err != nil {
return []Song{}, err
}
songs = append(songs, s)
}
return songs, nil
}
func scanSong(row *sql.Row) (Song, error) {
var s Song
if err := row.Scan(&s); err != nil {
return Song{}, err
}
return s, nil
}
func scanCollections(rows *sql.Rows) ([]Collection, error) {
var collection []Collection
for rows.Next() {
var c Collection
if err := rows.Scan(&c); err != nil {
return []Collection{}, err
}
collection = append(collection, c)
}
return collection, nil
}
func scanCollection(row *sql.Row) (Collection, error) {
var c Collection
if err := row.Scan(&c); err != nil {
return Collection{}, err
}
return c, nil
}
func scanCollectionPreviews(rows *sql.Rows) ([]CollectionPreview, error) {
var collection []CollectionPreview
for rows.Next() {
var c CollectionPreview
if err := rows.Scan(&c); err != nil {
return []CollectionPreview{}, err
}
collection = append(collection, c)
}
return collection, nil
}
func scanCollectionPreview(row *sql.Row) (CollectionPreview, error) {
var c CollectionPreview
if err := row.Scan(&c); err != nil {
return CollectionPreview{}, err
}
return c, nil
}

21
go-backend/go.mod Normal file
View File

@@ -0,0 +1,21 @@
module backend
go 1.23.5
require (
github.com/joho/godotenv v1.5.1
github.com/juli0n21/go-osu-parser v0.0.6
modernc.org/sqlite v1.34.5
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/sys v0.22.0 // indirect
modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
)

47
go-backend/go.sum Normal file
View File

@@ -0,0 +1,47 @@
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/juli0n21/go-osu-parser v0.0.6 h1:r5BTNrEDUHsF0ZFCvx0vfRcjU2IRvT3va4O1r3dm7og=
github.com/juli0n21/go-osu-parser v0.0.6/go.mod h1:oLLWnZReOMW4i5aNva/zvXsFqzdQigrbjyxOSs0cx+0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

187
go-backend/handlers.go Normal file
View File

@@ -0,0 +1,187 @@
package main
import (
"database/sql"
"errors"
"fmt"
"net/http"
"strconv"
"github.com/juli0n21/go-osu-parser/parser"
)
func run(db *sql.DB, osuDb *parser.OsuDB) {
http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "pong")
})
http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Login endpoint")
})
http.HandleFunc("/api/v1/songs/", func(w http.ResponseWriter, r *http.Request) {
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
fmt.Fprintln(w, err)
}
offset, err := strconv.Atoi(r.URL.Query().Get("offset"))
if err != nil {
fmt.Fprintln(w, err)
}
recent, err := getRecent(db, limit, offset)
if err != nil {
fmt.Fprintln(w, err)
}
fmt.Fprintln(w, recent)
})
http.HandleFunc("/api/v1/songs/recent", func(w http.ResponseWriter, r *http.Request) {
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
fmt.Fprintln(w, err)
}
offset, err := strconv.Atoi(r.URL.Query().Get("offset"))
if err != nil {
fmt.Fprintln(w, err)
}
recent, err := getRecent(db, limit, offset)
if err != nil {
fmt.Fprintln(w, err)
}
fmt.Fprintln(w, recent)
fmt.Fprintln(w, "Recent songs endpoint")
})
http.HandleFunc("/api/v1/songs/favorite", func(w http.ResponseWriter, r *http.Request) {
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
fmt.Fprintln(w, err)
}
offset, err := strconv.Atoi(r.URL.Query().Get("offset"))
if err != nil {
fmt.Fprintln(w, err)
}
recent, err := getRecent(db, limit, offset)
if err != nil {
fmt.Fprintln(w, err)
}
fmt.Fprintln(w, recent)
fmt.Fprintln(w, "Favorite songs endpoint")
})
http.HandleFunc("/api/v1/collections/{index}", func(w http.ResponseWriter, r *http.Request) {
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
fmt.Fprintln(w, err)
}
offset, err := strconv.Atoi(r.URL.Query().Get("offset"))
if err != nil {
fmt.Fprintln(w, err)
}
recent, err := getRecent(db, limit, offset)
if err != nil {
fmt.Fprintln(w, err)
}
fmt.Fprintln(w, recent)
fmt.Fprintln(w, "Collections endpoint")
})
http.HandleFunc("/api/v1/collections/", func(w http.ResponseWriter, r *http.Request) {
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
fmt.Fprintln(w, err)
}
offset, err := strconv.Atoi(r.URL.Query().Get("offset"))
if err != nil {
fmt.Fprintln(w, err)
}
recent, err := getRecent(db, limit, offset)
if err != nil {
fmt.Fprintln(w, err)
}
fmt.Fprintln(w, recent)
fmt.Fprintln(w, "Collection with index endpoint")
})
http.HandleFunc("/api/v1/audio/{hash}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Audio endpoint")
})
http.HandleFunc("/api/v1/search/active", func(w http.ResponseWriter, r *http.Request) {
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
fmt.Fprintln(w, err)
}
offset, err := strconv.Atoi(r.URL.Query().Get("offset"))
if err != nil {
fmt.Fprintln(w, err)
}
q := r.URL.Query().Get("query")
if err != nil || q == "" {
fmt.Fprintln(w, errors.New("'query' is required for search"))
}
recent, err := getSearch(db, q, limit, offset)
if err != nil {
fmt.Fprintln(w, err)
}
fmt.Fprintln(w, recent)
fmt.Fprintln(w, "Active search endpoint")
})
http.HandleFunc("/api/v1/search/artist", func(w http.ResponseWriter, r *http.Request) {
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
fmt.Fprintln(w, err)
}
offset, err := strconv.Atoi(r.URL.Query().Get("offset"))
if err != nil {
fmt.Fprintln(w, err)
}
q := r.URL.Query().Get("query")
if err != nil {
fmt.Fprintln(w, err)
}
recent, err := getArtists(db, q, limit, offset)
if err != nil {
fmt.Fprintln(w, err)
}
fmt.Fprintln(w, recent)
fmt.Fprintln(w, "Artist search endpoint")
})
http.HandleFunc("/api/v1/search/collections", func(w http.ResponseWriter, r *http.Request) {
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
fmt.Fprintln(w, err)
}
offset, err := strconv.Atoi(r.URL.Query().Get("offset"))
if err != nil {
fmt.Fprintln(w, err)
}
q := r.URL.Query().Get("query")
if err != nil {
fmt.Fprintln(w, err)
}
recent, err := getCollections(db, q, limit, offset)
if err != nil {
fmt.Fprintln(w, err)
}
fmt.Fprintln(w, recent)
})
http.HandleFunc("/api/v1/images/{filename}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Images endpoint")
})
fmt.Println("starting server on http://localhost:8080")
http.ListenAndServe(":8080", nil)
}

30
go-backend/main.go Normal file
View File

@@ -0,0 +1,30 @@
package main
import (
"fmt"
"log"
"github.com/joho/godotenv"
"github.com/juli0n21/go-osu-parser/parser"
)
func main() {
filename := "/mnt/g/Anwendungen/osu!/osu!.db"
if err := godotenv.Load(); err != nil {
fmt.Println("Error loading .env file")
}
osuDb, err := parser.ParseOsuDB(filename)
if err != nil {
log.Fatal(err)
}
db, err := initDB("./data/music.db", osuDb)
if err != nil {
log.Fatal(err)
}
run(db, osuDb)
}

35
go-backend/models.go Normal file
View File

@@ -0,0 +1,35 @@
package main
import "time"
type Song struct {
MD5Hash string
Title string
Artist string
Creator string
Folder string
File string
Audio string
Mapper string
TotalTime time.Duration
Url string
Image string
}
type CollectionPreview struct {
name string
image string
index int
items int
}
type Collection struct {
name string
items int
Songs []Song
}
type ActiveSearch struct {
artist string
Songs []Song
}