From 396ccc28f4b94af187f7006df405e000b181c263 Mon Sep 17 00:00:00 2001 From: JuLi0n21 Date: Wed, 21 May 2025 01:20:56 +0200 Subject: [PATCH] actual changes :3 --- go-backend/docs/docs.go | 4 +- go-backend/docs/swagger.json | 2 +- go-backend/docs/swagger.yaml | 2 +- go-backend/go.mod | 2 +- go-backend/handlers.go | 60 ++- go-backend/main.go | 13 +- grpc-backend/.gitignore | 3 + grpc-backend/Makefile | 30 ++ grpc-backend/database.go | 599 +++++++++++++++++++++++++ grpc-backend/docs/docs.go | 683 +++++++++++++++++++++++++++++ grpc-backend/docs/swagger.json | 656 +++++++++++++++++++++++++++ grpc-backend/docs/swagger.yaml | 441 +++++++++++++++++++ grpc-backend/go.mod | 44 ++ grpc-backend/go.sum | 152 +++++++ grpc-backend/handlers.go | 446 +++++++++++++++++++ grpc-backend/main.go | 232 ++++++++++ grpc-backend/models.go | 46 ++ grpc-backend/proto/osu_music.proto | 162 +++++++ osu_music.proto | 162 +++++++ proxy/auth.go | 2 +- proxy/go.mod | 2 +- proxy/routes.go | 3 - 22 files changed, 3710 insertions(+), 36 deletions(-) create mode 100644 grpc-backend/.gitignore create mode 100644 grpc-backend/Makefile create mode 100644 grpc-backend/database.go create mode 100644 grpc-backend/docs/docs.go create mode 100644 grpc-backend/docs/swagger.json create mode 100644 grpc-backend/docs/swagger.yaml create mode 100644 grpc-backend/go.mod create mode 100644 grpc-backend/go.sum create mode 100644 grpc-backend/handlers.go create mode 100644 grpc-backend/main.go create mode 100644 grpc-backend/models.go create mode 100644 grpc-backend/proto/osu_music.proto create mode 100644 osu_music.proto diff --git a/go-backend/docs/docs.go b/go-backend/docs/docs.go index 92743d0..073c744 100644 --- a/go-backend/docs/docs.go +++ b/go-backend/docs/docs.go @@ -669,8 +669,8 @@ const docTemplate = `{ // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ Version: "1.0", - Host: "localhost:8080", - BasePath: "/api/v1/", + Host: "", + BasePath: "api/v1/", Schemes: []string{}, Title: "go-osu-music-hoster", Description: "Server Hosting ur own osu files over a simple Api", diff --git a/go-backend/docs/swagger.json b/go-backend/docs/swagger.json index 6da7744..0421b35 100644 --- a/go-backend/docs/swagger.json +++ b/go-backend/docs/swagger.json @@ -6,7 +6,7 @@ "contact": {}, "version": "1.0" }, - "host": "localhost:8080", + "host": "/", "basePath": "/api/v1/", "paths": { "/audio/{filepath}": { diff --git a/go-backend/docs/swagger.yaml b/go-backend/docs/swagger.yaml index 5eb557f..f3bb86c 100644 --- a/go-backend/docs/swagger.yaml +++ b/go-backend/docs/swagger.yaml @@ -69,7 +69,7 @@ definitions: example: 240 type: integer type: object -host: localhost:8080 +host: / info: contact: {} description: Server Hosting ur own osu files over a simple Api diff --git a/go-backend/go.mod b/go-backend/go.mod index 62193c0..9495f0a 100644 --- a/go-backend/go.mod +++ b/go-backend/go.mod @@ -1,6 +1,6 @@ module backend -go 1.23.5 +go 1.24.3 require ( github.com/joho/godotenv v1.5.1 diff --git a/go-backend/handlers.go b/go-backend/handlers.go index 7974160..795261e 100644 --- a/go-backend/handlers.go +++ b/go-backend/handlers.go @@ -31,32 +31,58 @@ type Server struct { Env map[string]string } -func (s *Server) registerRoutes() { - http.HandleFunc("/api/v1/ping", s.ping) - http.HandleFunc("/api/v1/login", s.login) +func (s *Server) registerRoutes() http.Handler { + mux := http.NewServeMux() - http.HandleFunc("/api/v1/song/{hash}/", s.song) - http.HandleFunc("/api/v1/songs/recents", s.recents) - http.HandleFunc("/api/v1/songs/favorites", s.favorites) - http.HandleFunc("/api/v1/songs/artist", s.aristsSongs) + mux.HandleFunc("/api/v1/ping", s.ping) + mux.HandleFunc("/api/v1/login", s.login) - http.HandleFunc("/api/v1/collection", s.collection) - http.HandleFunc("/api/v1/search/collections", s.collectionSearch) + mux.HandleFunc("/api/v1/song/{hash}/", s.song) + mux.HandleFunc("/api/v1/songs/recent", s.recents) + mux.HandleFunc("/api/v1/songs/favorites", s.favorites) + mux.HandleFunc("/api/v1/songs/artist", s.aristsSongs) - http.HandleFunc("/api/v1/search/active", s.activeSearch) - http.HandleFunc("/api/v1/search/artist", s.artistSearch) + mux.HandleFunc("/api/v1/collection", s.collection) + mux.HandleFunc("/api/v1/search/collections", s.collectionSearch) - http.HandleFunc("/api/v1/audio/{filepath}", s.songFile) - http.HandleFunc("/api/v1/image/{filepath}", s.imageFile) + mux.HandleFunc("/api/v1/search/active", s.activeSearch) + mux.HandleFunc("/api/v1/search/artist", s.artistSearch) - http.HandleFunc("/api/v1/callback", s.callback) - http.Handle("/swagger/", httpSwagger.WrapHandler) + mux.HandleFunc("/api/v1/audio/{filepath}", s.songFile) + mux.HandleFunc("/api/v1/image/{filepath}", s.imageFile) + + mux.HandleFunc("/api/v1/callback", s.callback) + mux.Handle("/swagger/", httpSwagger.WrapHandler) + + return corsMiddleware(logRequests(mux)) } func run(s *Server) { - s.registerRoutes() + mux := s.registerRoutes() fmt.Println("starting server on http://localhost" + s.Port) - log.Fatal(http.ListenAndServe(s.Port, nil)) + log.Fatal(http.ListenAndServe(s.Port, mux)) +} + +func logRequests(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Printf("%s %s %s", r.RemoteAddr, r.Method, r.URL.Path) + next.ServeHTTP(w, r) + }) +} + +func corsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + + next.ServeHTTP(w, r) + }) } // ping godoc diff --git a/go-backend/main.go b/go-backend/main.go index 2a97279..a5f329c 100644 --- a/go-backend/main.go +++ b/go-backend/main.go @@ -21,10 +21,9 @@ import ( // @version 1.0 // @description Server Hosting ur own osu files over a simple Api -// @host localhost:8080 +// @host / // @BasePath /api/v1/ func main() { - envMap, err := godotenv.Read(".env") if err != nil { fmt.Println("Error reading .env file") @@ -125,7 +124,7 @@ func StartCloudflared(port string) (string, error) { } func sendUrl(endpoint, cookie string) error { - url := "http://proxy.illegalesachen.download/settings" + url := GetEnv("PROXY_URL", "https://proxy.illegalesachen.download/settings") payload := struct { Sharing *bool `json:"sharing"` @@ -137,22 +136,19 @@ func sendUrl(endpoint, cookie string) error { body, err := json.Marshal(payload) if err != nil { return fmt.Errorf("Error marshalling payload: %v", err) - } req, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) if err != nil { - return fmt.Errorf("Error creating request: %v", err) + return fmt.Errorf("failed to create request: %v", err) } + req.Header.Add("Content-Type", "application/json") req.AddCookie(&http.Cookie{ Name: "session_cookie", Value: cookie, }) - req.Header.Set("Content-Type", "application/json") - - fmt.Println(req) client := &http.Client{} resp, err := client.Do(req) if err != nil { @@ -163,6 +159,5 @@ func sendUrl(endpoint, cookie string) error { if resp.StatusCode != http.StatusOK { return fmt.Errorf("Error in request: %s", resp.Status) } - fmt.Println("Response Status:", resp.Status) return nil } diff --git a/grpc-backend/.gitignore b/grpc-backend/.gitignore new file mode 100644 index 0000000..6862c21 --- /dev/null +++ b/grpc-backend/.gitignore @@ -0,0 +1,3 @@ +data/ +gen/ +*.env \ No newline at end of file diff --git a/grpc-backend/Makefile b/grpc-backend/Makefile new file mode 100644 index 0000000..d2bd858 --- /dev/null +++ b/grpc-backend/Makefile @@ -0,0 +1,30 @@ +# Paths +PROTO_DIR = proto +GOOGLEAPIS_DIR = googleapis +OUT_DIR = gen + +PROTO_FILE = osu_music.proto + +# Protoc plugins (assumed in PATH) +PROTOC ?= protoc +PROTOC_GEN_GO ?= protoc-gen-go +PROTOC_GEN_GO_GRPC ?= protoc-gen-go-grpc +PROTOC_GEN_GRPC_GATEWAY ?= protoc-gen-grpc-gateway +PROTOC_GEN_OPENAPIV2 ?= protoc-gen-openapiv2 + +all: generate + +generate: + $(PROTOC) \ + -I $(PROTO_DIR) \ + -I $(GOOGLEAPIS_DIR) \ + --go_out $(OUT_DIR) --go_opt paths=source_relative \ + --go-grpc_out $(OUT_DIR) --go-grpc_opt paths=source_relative \ + --grpc-gateway_out $(OUT_DIR) --grpc-gateway_opt paths=source_relative \ + --openapiv2_out $(OUT_DIR)/swagger \ + $(PROTO_DIR)/$(PROTO_FILE) + +clean: + rm -rf $(OUT_DIR) + +.PHONY: all generate clean \ No newline at end of file diff --git a/grpc-backend/database.go b/grpc-backend/database.go new file mode 100644 index 0000000..49a075a --- /dev/null +++ b/grpc-backend/database.go @@ -0,0 +1,599 @@ +package main + +import ( + "database/sql" + "errors" + "fmt" + "log" + "os" + "path" + "path/filepath" + "strconv" + "strings" + + "github.com/juli0n21/go-osu-parser/parser" + _ "modernc.org/sqlite" +) + +var ErrBeatmapCountNotMatch = errors.New("beatmap count not matching") + +var osuDB *parser.OsuDB +var osuRoot string + +func initDB(connectionString string, osuDb *parser.OsuDB, osuroot string) (*sql.DB, error) { + + osuDB = osuDb + osuRoot = osuroot + + 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) + } + + _, err = db.Exec("PRAGMA temp_store = MEMORY;") + if err != nil { + log.Fatal(err) + } + + if err = createDB(db); err != nil { + return nil, err + } + + if err = checkhealth(db, osuDB); err != nil { + if err = rebuildBeatmapDb(db, osuDB); err != nil { + return nil, err + } + } + + if err = createCollectionDB(db); err != nil { + return nil, err + } + + collectionDB, err := parser.ParseCollectionsDB(path.Join(osuRoot, "collection.db")) + if err != nil { + return nil, err + } + + if err = checkCollectionHealth(db, collectionDB); err != nil { + if err = rebuildCollectionDb(db, collectionDB); err != nil { + return nil, err + } + } + + return db, nil +} + +func createDB(db *sql.DB) error { + + _, err := db.Exec(` + 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', + Audio TEXT DEFAULT 'unknown.mp3', + MD5Hash TEXT DEFAULT '00000000000000000000000000000000', + File 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', + Folder TEXT DEFAULT 'Unknown Folder', + UNIQUE (Artist, Title, MD5Hash, Difficulty) + ); + `) + if err != nil { + return err + } + + _, err = db.Exec("CREATE INDEX IF NOT EXISTS idx_beatmap_md5hash ON Beatmap(MD5Hash);") + if err != nil { + return err + } + + _, err = db.Exec("CREATE INDEX IF NOT EXISTS idx_beatmap_lastModifiedTime ON Beatmap(LastModifiedTime);") + if err != nil { + return err + } + + _, err = db.Exec("CREATE INDEX IF NOT EXISTS idx_beatmap_title_artist ON Beatmap(Title, Artist);") + if err != nil { + return err + } + + return nil +} + +func createCollectionDB(db *sql.DB) error { + _, err := db.Exec(`CREATE TABLE IF NOT EXISTS Collection ( + Name TEXT DEFAULT '', + MD5Hash TEXT DEFAULT '00000000000000000000000000000000' + ); + `) + if err != nil { + return err + } + + _, err = db.Exec("CREATE INDEX IF NOT EXISTS idx_collection_name ON Collection(Name);") + if err != nil { + return err + } + + _, err = db.Exec("CREATE INDEX IF NOT EXISTS idx_collection_md5hash ON Collection(MD5Hash);") + 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 rebuildBeatmapDb(db *sql.DB, osuDb *parser.OsuDB) error { + + if _, err := db.Exec("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, Audio, MD5Hash, File, RankedStatus, + LastModifiedTime, TotalTime, AudioPreviewTime, BeatmapSetId, + Source, Tags, LastPlayed, Folder + ) 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 checkCollectionHealth(db *sql.DB, collectionDB *parser.Collections) error { + rows, err := db.Query(`SELECT COUNT(*) FROM Collection GROUP BY Name;`) + if err != nil { + return err + } + defer rows.Close() + + var count int + if err = rows.Scan(&count); err != nil { + return err + } + + if count != int(collectionDB.NumberOfCollections) { + return errors.New("Collection Count Not Matching") + } + + rows, err = db.Query(`SELECT COUNT(*) FROM Collection;`) + if err != nil { + return err + } + + if err = rows.Scan(&count); err != nil { + return err + } + + sum := 0 + for _, col := range collectionDB.Collections { + sum += len(col.Beatmaps) + } + + if count != int(sum) { + return errors.New("Beatmap count missmatch rebuilding collections") + } + + return nil +} + +func rebuildCollectionDb(db *sql.DB, collectionDb *parser.Collections) error { + if _, err := db.Exec("DROP TABLE Collection"); err != nil { + return err + } + + if err := createCollectionDB(db); err != nil { + return err + } + + stmt, err := db.Prepare(` + INSERT INTO Collection ( + Name, + MD5Hash + ) VALUES (?, ?) + `) + if err != nil { + return err + } + defer stmt.Close() + + tx, err := db.Begin() + if err != nil { + return err + } + + stmt = tx.Stmt(stmt) + + for _, col := range collectionDb.Collections { + for _, hash := range col.Beatmaps { + _, err := stmt.Exec(col.Name, hash) + if err != nil { + fmt.Println(err) + } + } + } + + return tx.Commit() +} + +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 BeatmapId, MD5Hash, Title, Artist, Creator, Folder, File, Audio, TotalTime FROM Beatmap ORDER BY LastModifiedTime DESC LIMIT ? OFFSET ?", limit, offset) + if err != nil { + return []Song{}, err + } + defer rows.Close() + + return scanSongs(rows) +} + +func getSearch(db *sql.DB, q string, limit, offset int) (ActiveSearch, error) { + rows, err := db.Query("SELECT BeatmapId, MD5Hash, Title, Artist, Creator, Folder, File, Audio, TotalTime FROM Beatmap WHERE MD5Hash 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) ([]Artist, error) { + rows, err := db.Query("SELECT Artist, COUNT(Artist) FROM Beatmap WHERE Artist LIKE ? OR Title LIKE ? GROUP BY Artist LIMIT ? OFFSET ?", "%"+q+"%", "%"+q+"%", limit, offset) + if err != nil { + return []Artist{}, err + } + defer rows.Close() + + artist := []Artist{} + for rows.Next() { + var a string + var c int + err := rows.Scan(&a, &c) + if err != nil { + return []Artist{}, err + } + artist = append(artist, Artist{Artist: a, Count: c}) + } + + return artist, 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, limit, offset, index int) (Collection, error) { + rows, err := db.Query(` + WITH cols AS ( + SELECT + c.Name, + ROW_NUMBER() OVER (ORDER BY c.Name) AS RowNumber + FROM Collection c + GROUP BY c.Name + ) + + SELECT + c.Name, b.BeatmapId, b.MD5Hash, b.Title, b.Artist, + b.Creator, b.Folder, b.File, b.Audio, b.TotalTime + FROM Collection c + Join Beatmap b ON c.MD5Hash = b.MD5Hash + WHERE c.Name = (SELECT Name FROM cols WHERE RowNumber = ?) + LIMIT ? + OFFSET ?;`, index, limit, offset) + if err != nil { + return Collection{}, err + } + defer rows.Close() + + var c Collection + for rows.Next() { + s := Song{} + if err := rows.Scan(&c.Name, &s.BeatmapID, &s.MD5Hash, &s.Title, &s.Artist, &s.Creator, &s.Folder, &s.File, &s.Audio, &s.TotalTime); err != nil { + return Collection{}, err + } + + s.Image = extractImageFromFile(osuRoot, s.Folder, s.File) + + c.Songs = append(c.Songs, s) + } + + row := db.QueryRow(`SELECT COUNT(*) FROM Collection WHERE Name = ?`, c.Name) + var count string + row.Scan(&count) + + if i, err := strconv.Atoi(count); err == nil { + c.Items = i + } + + return c, nil +} + +func getCollectionByName(db *sql.DB, limit, offset int, name string) (Collection, error) { + rows, err := db.Query(` + SELECT + c.Name, b.BeatmapId, b.MD5Hash, b.Title, b.Artist, + b.Creator, b.Folder, b.File, b.Audio, b.TotalTime + FROM Collection c + Join Beatmap b ON c.MD5Hash = b.MD5Hash + WHERE c.Name = ? + LIMIT ? + OFFSET ?;`, name, limit, offset) + if err != nil { + return Collection{}, err + } + defer rows.Close() + + var c Collection + for rows.Next() { + s := Song{} + if err := rows.Scan(&c.Name, &s.BeatmapID, &s.MD5Hash, &s.Title, &s.Artist, &s.Creator, &s.Folder, &s.File, &s.Audio, &s.TotalTime); err != nil { + return Collection{}, err + } + + s.Image = extractImageFromFile(osuRoot, s.Folder, s.File) + + c.Songs = append(c.Songs, s) + } + + row := db.QueryRow(`SELECT COUNT(*) FROM Collection WHERE Name = ?`, c.Name) + var count string + row.Scan(&count) + + if i, err := strconv.Atoi(count); err == nil { + c.Items = i + } else { + return Collection{}, err + } + + return c, nil +} + +func getCollections(db *sql.DB, q string, limit, offset int) ([]CollectionPreview, error) { + rows, err := db.Query(` + SELECT + c.Name, COUNT(b.MD5Hash), b.Folder, b.File + FROM Collection c + Join Beatmap b ON c.MD5Hash = b.MD5Hash + WHERE c.Name LIKE ? + GROUP BY c.NAME + LIMIT ? + OFFSET ?;`, "%"+q+"%", limit, offset) + if err != nil { + return []CollectionPreview{}, err + } + defer rows.Close() + + var collections []CollectionPreview + for rows.Next() { + var c CollectionPreview + var folder, file, count string + if err := rows.Scan(&c.Name, &count, &folder, &file); err != nil { + return []CollectionPreview{}, err + } + + if i, err := strconv.Atoi(count); err == nil { + c.Items = i + } + c.Image = extractImageFromFile(osuRoot, folder, file) + + collections = append(collections, c) + } + + return collections, nil +} + +func getSong(db *sql.DB, hash string) (Song, error) { + + row := db.QueryRow("SELECT BeatmapId, MD5Hash, Title, Artist, Creator, Folder, File, Audio, TotalTime FROM Beatmap WHERE MD5Hash = ?", hash) + s, err := scanSong(row) + return s, err + +} + +func scanSongs(rows *sql.Rows) ([]Song, error) { + songs := []Song{} + for rows.Next() { + var s Song + if err := rows.Scan(&s.BeatmapID, &s.MD5Hash, &s.Title, &s.Artist, &s.Creator, &s.Folder, &s.File, &s.Audio, &s.TotalTime); err != nil { + return []Song{}, err + } + + bm, err := parser.ParseOsuFile(fmt.Sprintf("%sSongs/%s/%s", osuRoot, s.Folder, s.File)) + if err != nil { + fmt.Println(err) + s.Image = fmt.Sprintf("404.png") + } else { + if len(bm.Events) > 1 && len(bm.Events[0].EventParams) > 1 { + s.Image = fmt.Sprintf("%s/%s", s.Folder, strings.Trim(bm.Events[0].EventParams[0], "\"")) + } + } + + songs = append(songs, s) + } + return songs, nil +} + +func scanSong(row *sql.Row) (Song, error) { + + s := Song{} + if err := row.Scan(&s.BeatmapID, &s.MD5Hash, &s.Title, &s.Artist, &s.Creator, &s.Folder, &s.File, &s.Audio, &s.TotalTime); err != nil { + return Song{}, err + } + + s.Image = extractImageFromFile(osuRoot, s.Folder, s.File) + + return s, nil +} + +func extractImageFromFile(osuRoot, folder, file string) string { + bm, err := parser.ParseOsuFile(filepath.Join(osuRoot, "Songs", folder, file)) + if err != nil { + fmt.Println(err) + return "404.png" + } + + if bm.Version > 3 { + if len(bm.Events) > 0 && len(bm.Events[0].EventParams) > 0 { + return fmt.Sprintf(`%s/%s`, folder, strings.Trim(bm.Events[0].EventParams[0], "\"")) + } + } else { + fmt.Println(bm.Events) + } + + return "404.png" +} + +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 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 +} diff --git a/grpc-backend/docs/docs.go b/grpc-backend/docs/docs.go new file mode 100644 index 0000000..5f06b61 --- /dev/null +++ b/grpc-backend/docs/docs.go @@ -0,0 +1,683 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/audio/{filepath}": { + "get": { + "description": "Retrieves a song file from the server based on the provided encoded filepath", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "files" + ], + "summary": "Retrieves a song file by its encoded path", + "parameters": [ + { + "type": "string", + "description": "Base64 encoded file path", + "name": "filepath", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "The requested song file", + "schema": { + "type": "file" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "404": { + "description": "File Not Found", + "schema": { + "type": "string" + } + } + } + } + }, + "/collection": { + "get": { + "description": "Retrieves a collection of songs using the provided index.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "songs" + ], + "summary": "Get a collection of songs by index", + "parameters": [ + { + "type": "integer", + "description": "Index", + "name": "index", + "in": "query" + }, + { + "type": "string", + "description": "Index", + "name": "name", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Song" + } + } + }, + "400": { + "description": "Invalid parameter", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + }, + "/image/{filepath}": { + "get": { + "description": "Retrieves an image file from the server based on the provided encoded filepath", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "files" + ], + "summary": "Retrieves an image file by its encoded path", + "parameters": [ + { + "type": "string", + "description": "Base64 encoded file path", + "name": "filepath", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "The requested image file", + "schema": { + "type": "file" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "404": { + "description": "File Not Found", + "schema": { + "type": "string" + } + } + } + } + }, + "/login": { + "get": { + "description": "Redirects users to an external authentication page", + "tags": [ + "auth" + ], + "summary": "Redirect to login page", + "responses": { + "307": { + "description": "Temporary Redirect", + "schema": { + "type": "string" + } + } + } + } + }, + "/ping": { + "get": { + "description": "Returns a pong response if the server is running", + "tags": [ + "health" + ], + "summary": "Check server health", + "responses": { + "200": { + "description": "pong", + "schema": { + "type": "string" + } + } + } + } + }, + "/search/active": { + "get": { + "description": "Searches active records in the database based on the query parameter", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "search" + ], + "summary": "Searches active records based on a query", + "parameters": [ + { + "type": "string", + "description": "Search query", + "name": "query", + "in": "query", + "required": true + }, + { + "type": "integer", + "default": 10, + "description": "Limit the number of results", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset for pagination", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Active search result", + "schema": { + "$ref": "#/definitions/main.ActiveSearch" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/search/artist": { + "get": { + "description": "Searches for artists in the database based on the query parameter", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "search" + ], + "summary": "Searches for artists based on a query", + "parameters": [ + { + "type": "string", + "description": "Search query", + "name": "query", + "in": "query", + "required": true + }, + { + "type": "integer", + "default": 10, + "description": "Limit the number of results", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset for pagination", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "List of artists", + "schema": { + "$ref": "#/definitions/main.Artist" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/search/collections": { + "get": { + "description": "Searches collections in the database based on the query parameter", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "search" + ], + "summary": "Searches collections based on a query", + "parameters": [ + { + "type": "string", + "description": "Search query", + "name": "query", + "in": "query", + "required": true + }, + { + "type": "integer", + "default": 10, + "description": "Limit the number of results", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset for pagination", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "List of collections", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Collection" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/song/{hash}": { + "get": { + "description": "Retrieves a song using its unique hash identifier.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "songs" + ], + "summary": "Get a song by its hash", + "parameters": [ + { + "type": "string", + "description": "Song hash", + "name": "hash", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.Song" + } + }, + "400": { + "description": "Invalid parameter", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Song not found", + "schema": { + "type": "string" + } + } + } + } + }, + "/songs/artist": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "songs" + ], + "summary": "Returns all the Songs of a specific Artist", + "parameters": [ + { + "type": "string", + "description": "Artist Name", + "name": "artist", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Song" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/songs/favorites": { + "get": { + "description": "Retrieves favorite songs filtered by a query with pagination support.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "songs" + ], + "summary": "Get a list of favorite songs based on a query", + "parameters": [ + { + "type": "string", + "description": "Search query", + "name": "query", + "in": "query", + "required": true + }, + { + "type": "integer", + "default": 10, + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Song" + } + } + }, + "400": { + "description": "Invalid parameter", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + }, + "/songs/recents": { + "get": { + "description": "Retrieves recent songs with pagination support.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "songs" + ], + "summary": "Get a list of recent songs", + "parameters": [ + { + "type": "integer", + "default": 10, + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Song" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + } + }, + "definitions": { + "main.ActiveSearch": { + "description": "ActiveSearch holds search results for a given artist", + "type": "object", + "properties": { + "artist": { + "type": "string", + "example": "Ed Sheeran" + }, + "songs": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Song" + } + } + } + }, + "main.Artist": { + "description": "Artist holds search results for a given artist", + "type": "object", + "properties": { + "artist": { + "type": "string", + "example": "Miku" + }, + "count": { + "type": "integer", + "example": 21 + } + } + }, + "main.Collection": { + "description": "Collection holds a list of songs", + "type": "object", + "properties": { + "items": { + "type": "integer", + "example": 15 + }, + "name": { + "type": "string", + "example": "Best of 2023" + }, + "songs": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Song" + } + } + } + }, + "main.Song": { + "description": "Song represents a song with metadata", + "type": "object", + "properties": { + "artist": { + "type": "string", + "example": "Ed Sheeran" + }, + "audio": { + "type": "string", + "example": "audio.mp3" + }, + "beatmap_id": { + "type": "integer", + "example": 123456 + }, + "creator": { + "type": "string", + "example": "JohnDoe" + }, + "file": { + "type": "string", + "example": "beatmap.osu" + }, + "folder": { + "type": "string", + "example": "osu/Songs/123456" + }, + "image": { + "type": "string", + "example": "cover.jpg" + }, + "md5_hash": { + "type": "string", + "example": "abcd1234efgh5678" + }, + "title": { + "type": "string", + "example": "Shape of You" + }, + "total_time": { + "type": "integer", + "example": 240 + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "", + Description: "", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/grpc-backend/docs/swagger.json b/grpc-backend/docs/swagger.json new file mode 100644 index 0000000..7177a98 --- /dev/null +++ b/grpc-backend/docs/swagger.json @@ -0,0 +1,656 @@ +{ + "swagger": "2.0", + "info": { + "contact": {} + }, + "paths": { + "/audio/{filepath}": { + "get": { + "description": "Retrieves a song file from the server based on the provided encoded filepath", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "files" + ], + "summary": "Retrieves a song file by its encoded path", + "parameters": [ + { + "type": "string", + "description": "Base64 encoded file path", + "name": "filepath", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "The requested song file", + "schema": { + "type": "file" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "404": { + "description": "File Not Found", + "schema": { + "type": "string" + } + } + } + } + }, + "/collection": { + "get": { + "description": "Retrieves a collection of songs using the provided index.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "songs" + ], + "summary": "Get a collection of songs by index", + "parameters": [ + { + "type": "integer", + "description": "Index", + "name": "index", + "in": "query" + }, + { + "type": "string", + "description": "Index", + "name": "name", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Song" + } + } + }, + "400": { + "description": "Invalid parameter", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + }, + "/image/{filepath}": { + "get": { + "description": "Retrieves an image file from the server based on the provided encoded filepath", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "files" + ], + "summary": "Retrieves an image file by its encoded path", + "parameters": [ + { + "type": "string", + "description": "Base64 encoded file path", + "name": "filepath", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "The requested image file", + "schema": { + "type": "file" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "404": { + "description": "File Not Found", + "schema": { + "type": "string" + } + } + } + } + }, + "/login": { + "get": { + "description": "Redirects users to an external authentication page", + "tags": [ + "auth" + ], + "summary": "Redirect to login page", + "responses": { + "307": { + "description": "Temporary Redirect", + "schema": { + "type": "string" + } + } + } + } + }, + "/ping": { + "get": { + "description": "Returns a pong response if the server is running", + "tags": [ + "health" + ], + "summary": "Check server health", + "responses": { + "200": { + "description": "pong", + "schema": { + "type": "string" + } + } + } + } + }, + "/search/active": { + "get": { + "description": "Searches active records in the database based on the query parameter", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "search" + ], + "summary": "Searches active records based on a query", + "parameters": [ + { + "type": "string", + "description": "Search query", + "name": "query", + "in": "query", + "required": true + }, + { + "type": "integer", + "default": 10, + "description": "Limit the number of results", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset for pagination", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Active search result", + "schema": { + "$ref": "#/definitions/main.ActiveSearch" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/search/artist": { + "get": { + "description": "Searches for artists in the database based on the query parameter", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "search" + ], + "summary": "Searches for artists based on a query", + "parameters": [ + { + "type": "string", + "description": "Search query", + "name": "query", + "in": "query", + "required": true + }, + { + "type": "integer", + "default": 10, + "description": "Limit the number of results", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset for pagination", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "List of artists", + "schema": { + "$ref": "#/definitions/main.Artist" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/search/collections": { + "get": { + "description": "Searches collections in the database based on the query parameter", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "search" + ], + "summary": "Searches collections based on a query", + "parameters": [ + { + "type": "string", + "description": "Search query", + "name": "query", + "in": "query", + "required": true + }, + { + "type": "integer", + "default": 10, + "description": "Limit the number of results", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset for pagination", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "List of collections", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Collection" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/song/{hash}": { + "get": { + "description": "Retrieves a song using its unique hash identifier.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "songs" + ], + "summary": "Get a song by its hash", + "parameters": [ + { + "type": "string", + "description": "Song hash", + "name": "hash", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.Song" + } + }, + "400": { + "description": "Invalid parameter", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Song not found", + "schema": { + "type": "string" + } + } + } + } + }, + "/songs/artist": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "songs" + ], + "summary": "Returns all the Songs of a specific Artist", + "parameters": [ + { + "type": "string", + "description": "Artist Name", + "name": "artist", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Song" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/songs/favorites": { + "get": { + "description": "Retrieves favorite songs filtered by a query with pagination support.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "songs" + ], + "summary": "Get a list of favorite songs based on a query", + "parameters": [ + { + "type": "string", + "description": "Search query", + "name": "query", + "in": "query", + "required": true + }, + { + "type": "integer", + "default": 10, + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Song" + } + } + }, + "400": { + "description": "Invalid parameter", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + }, + "/songs/recents": { + "get": { + "description": "Retrieves recent songs with pagination support.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "songs" + ], + "summary": "Get a list of recent songs", + "parameters": [ + { + "type": "integer", + "default": 10, + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Song" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + } + }, + "definitions": { + "main.ActiveSearch": { + "description": "ActiveSearch holds search results for a given artist", + "type": "object", + "properties": { + "artist": { + "type": "string", + "example": "Ed Sheeran" + }, + "songs": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Song" + } + } + } + }, + "main.Artist": { + "description": "Artist holds search results for a given artist", + "type": "object", + "properties": { + "artist": { + "type": "string", + "example": "Miku" + }, + "count": { + "type": "integer", + "example": 21 + } + } + }, + "main.Collection": { + "description": "Collection holds a list of songs", + "type": "object", + "properties": { + "items": { + "type": "integer", + "example": 15 + }, + "name": { + "type": "string", + "example": "Best of 2023" + }, + "songs": { + "type": "array", + "items": { + "$ref": "#/definitions/main.Song" + } + } + } + }, + "main.Song": { + "description": "Song represents a song with metadata", + "type": "object", + "properties": { + "artist": { + "type": "string", + "example": "Ed Sheeran" + }, + "audio": { + "type": "string", + "example": "audio.mp3" + }, + "beatmap_id": { + "type": "integer", + "example": 123456 + }, + "creator": { + "type": "string", + "example": "JohnDoe" + }, + "file": { + "type": "string", + "example": "beatmap.osu" + }, + "folder": { + "type": "string", + "example": "osu/Songs/123456" + }, + "image": { + "type": "string", + "example": "cover.jpg" + }, + "md5_hash": { + "type": "string", + "example": "abcd1234efgh5678" + }, + "title": { + "type": "string", + "example": "Shape of You" + }, + "total_time": { + "type": "integer", + "example": 240 + } + } + } + } +} \ No newline at end of file diff --git a/grpc-backend/docs/swagger.yaml b/grpc-backend/docs/swagger.yaml new file mode 100644 index 0000000..d2f91db --- /dev/null +++ b/grpc-backend/docs/swagger.yaml @@ -0,0 +1,441 @@ +definitions: + main.ActiveSearch: + description: ActiveSearch holds search results for a given artist + properties: + artist: + example: Ed Sheeran + type: string + songs: + items: + $ref: '#/definitions/main.Song' + type: array + type: object + main.Artist: + description: Artist holds search results for a given artist + properties: + artist: + example: Miku + type: string + count: + example: 21 + type: integer + type: object + main.Collection: + description: Collection holds a list of songs + properties: + items: + example: 15 + type: integer + name: + example: Best of 2023 + type: string + songs: + items: + $ref: '#/definitions/main.Song' + type: array + type: object + main.Song: + description: Song represents a song with metadata + properties: + artist: + example: Ed Sheeran + type: string + audio: + example: audio.mp3 + type: string + beatmap_id: + example: 123456 + type: integer + creator: + example: JohnDoe + type: string + file: + example: beatmap.osu + type: string + folder: + example: osu/Songs/123456 + type: string + image: + example: cover.jpg + type: string + md5_hash: + example: abcd1234efgh5678 + type: string + title: + example: Shape of You + type: string + total_time: + example: 240 + type: integer + type: object +info: + contact: {} +paths: + /audio/{filepath}: + get: + consumes: + - application/json + description: Retrieves a song file from the server based on the provided encoded + filepath + parameters: + - description: Base64 encoded file path + in: path + name: filepath + required: true + type: string + produces: + - application/json + responses: + "200": + description: The requested song file + schema: + type: file + "400": + description: Bad Request + schema: + type: string + "404": + description: File Not Found + schema: + type: string + summary: Retrieves a song file by its encoded path + tags: + - files + /collection: + get: + consumes: + - application/json + description: Retrieves a collection of songs using the provided index. + parameters: + - description: Index + in: query + name: index + type: integer + - description: Index + in: query + name: name + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/main.Song' + type: array + "400": + description: Invalid parameter + schema: + type: string + "500": + description: Internal server error + schema: + type: string + summary: Get a collection of songs by index + tags: + - songs + /image/{filepath}: + get: + consumes: + - application/json + description: Retrieves an image file from the server based on the provided encoded + filepath + parameters: + - description: Base64 encoded file path + in: path + name: filepath + required: true + type: string + produces: + - application/json + responses: + "200": + description: The requested image file + schema: + type: file + "400": + description: Bad Request + schema: + type: string + "404": + description: File Not Found + schema: + type: string + summary: Retrieves an image file by its encoded path + tags: + - files + /login: + get: + description: Redirects users to an external authentication page + responses: + "307": + description: Temporary Redirect + schema: + type: string + summary: Redirect to login page + tags: + - auth + /ping: + get: + description: Returns a pong response if the server is running + responses: + "200": + description: pong + schema: + type: string + summary: Check server health + tags: + - health + /search/active: + get: + consumes: + - application/json + description: Searches active records in the database based on the query parameter + parameters: + - description: Search query + in: query + name: query + required: true + type: string + - default: 10 + description: Limit the number of results + in: query + name: limit + type: integer + - default: 0 + description: Offset for pagination + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: Active search result + schema: + $ref: '#/definitions/main.ActiveSearch' + "400": + description: Bad Request + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Searches active records based on a query + tags: + - search + /search/artist: + get: + consumes: + - application/json + description: Searches for artists in the database based on the query parameter + parameters: + - description: Search query + in: query + name: query + required: true + type: string + - default: 10 + description: Limit the number of results + in: query + name: limit + type: integer + - default: 0 + description: Offset for pagination + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: List of artists + schema: + $ref: '#/definitions/main.Artist' + "400": + description: Bad Request + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Searches for artists based on a query + tags: + - search + /search/collections: + get: + consumes: + - application/json + description: Searches collections in the database based on the query parameter + parameters: + - description: Search query + in: query + name: query + required: true + type: string + - default: 10 + description: Limit the number of results + in: query + name: limit + type: integer + - default: 0 + description: Offset for pagination + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: List of collections + schema: + items: + $ref: '#/definitions/main.Collection' + type: array + "400": + description: Bad Request + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Searches collections based on a query + tags: + - search + /song/{hash}: + get: + consumes: + - application/json + description: Retrieves a song using its unique hash identifier. + parameters: + - description: Song hash + in: path + name: hash + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/main.Song' + "400": + description: Invalid parameter + schema: + type: string + "404": + description: Song not found + schema: + type: string + summary: Get a song by its hash + tags: + - songs + /songs/artist: + get: + consumes: + - application/json + parameters: + - description: Artist Name + in: query + name: artist + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/main.Song' + type: array + "400": + description: Bad Request + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Returns all the Songs of a specific Artist + tags: + - songs + /songs/favorites: + get: + consumes: + - application/json + description: Retrieves favorite songs filtered by a query with pagination support. + parameters: + - description: Search query + in: query + name: query + required: true + type: string + - default: 10 + description: Limit + in: query + name: limit + type: integer + - default: 0 + description: Offset + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/main.Song' + type: array + "400": + description: Invalid parameter + schema: + type: string + "500": + description: Internal server error + schema: + type: string + summary: Get a list of favorite songs based on a query + tags: + - songs + /songs/recents: + get: + consumes: + - application/json + description: Retrieves recent songs with pagination support. + parameters: + - default: 10 + description: Limit + in: query + name: limit + type: integer + - default: 0 + description: Offset + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/main.Song' + type: array + "500": + description: Internal server error + schema: + type: string + summary: Get a list of recent songs + tags: + - songs +swagger: "2.0" diff --git a/grpc-backend/go.mod b/grpc-backend/go.mod new file mode 100644 index 0000000..a86694c --- /dev/null +++ b/grpc-backend/go.mod @@ -0,0 +1,44 @@ +module backend + +go 1.23.5 + +toolchain go1.24.2 + +require ( + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 + github.com/joho/godotenv v1.5.1 + github.com/juli0n21/go-osu-parser v0.0.8 + github.com/swaggo/http-swagger v1.3.4 + google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 + google.golang.org/grpc v1.72.1 + google.golang.org/protobuf v1.36.6 + modernc.org/sqlite v1.37.0 +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/spec v0.20.6 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.6 // 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 + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect + github.com/swaggo/swag v1.8.1 // indirect + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/tools v0.31.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + modernc.org/libc v1.62.1 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.9.1 // indirect +) diff --git a/grpc-backend/go.sum b/grpc-backend/go.sum new file mode 100644 index 0000000..b859bf1 --- /dev/null +++ b/grpc-backend/go.sum @@ -0,0 +1,152 @@ +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/agiledragon/gomonkey/v2 v2.3.1 h1:k+UnUY0EMNYUFUAQVETGY9uUTxjMdnUkP0ARyJS1zzs= +github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +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/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/juli0n21/go-osu-parser v0.0.8 h1:aQtuhAniGvpUw446arhq/3aUOK9YvZEkL7aYUGlViAo= +github.com/juli0n21/go-osu-parser v0.0.8/go.mod h1:oLLWnZReOMW4i5aNva/zvXsFqzdQigrbjyxOSs0cx+0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE= +github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc= +github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= +github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww= +github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ= +github.com/swaggo/swag v1.8.1 h1:JuARzFX1Z1njbCGz+ZytBR15TFJwF2Q7fu8puJHhQYI= +github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 h1:IkAfh6J/yllPtpYFU0zZN1hUPYdT0ogkBT/9hMxHjvg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= +google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic= +modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU= +modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw= +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.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s= +modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g= +modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= +modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/grpc-backend/handlers.go b/grpc-backend/handlers.go new file mode 100644 index 0000000..da1bd35 --- /dev/null +++ b/grpc-backend/handlers.go @@ -0,0 +1,446 @@ +package main + +import ( + "database/sql" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "os" + "strconv" + + _ "backend/docs" + v1 "backend/gen" + + "github.com/joho/godotenv" + httpSwagger "github.com/swaggo/http-swagger" + + "github.com/juli0n21/go-osu-parser/parser" +) + +var ErrRequiredParameterNotPresent = errors.New("required parameter missing") +var ErrFailedToParseEncoded = errors.New("invalid encoded value") +var ErrFileNotFound = errors.New("file not found") + +type Server struct { + Port string + OsuDir string + Db *sql.DB + OsuDb *parser.OsuDB + Env map[string]string + + v1.UnimplementedMusicBackendServer +} + +func (s *Server) registerRoutes() http.Handler { + mux := http.NewServeMux() + + mux.HandleFunc("/api/v1/ping", s.ping) + mux.HandleFunc("/api/v1/login", s.login) + + mux.HandleFunc("/api/v1/song/{hash}/", s.song) + mux.HandleFunc("/api/v1/songs/recent", s.recents) + mux.HandleFunc("/api/v1/songs/favorites", s.favorites) + mux.HandleFunc("/api/v1/songs/artist", s.aristsSongs) + + mux.HandleFunc("/api/v1/collection", s.collection) + mux.HandleFunc("/api/v1/search/collections", s.collectionSearch) + + mux.HandleFunc("/api/v1/search/active", s.activeSearch) + mux.HandleFunc("/api/v1/search/artist", s.artistSearch) + + mux.HandleFunc("/api/v1/audio/{filepath}", s.songFile) + mux.HandleFunc("/api/v1/image/{filepath}", s.imageFile) + + mux.HandleFunc("/api/v1/callback", s.callback) + mux.Handle("/swagger/", httpSwagger.WrapHandler) + + return corsMiddleware(logRequests(mux)) +} + +func run(s *Server) { + mux := s.registerRoutes() + fmt.Println("starting server on http://localhost" + s.Port) + log.Fatal(http.ListenAndServe(s.Port, mux)) +} + +func logRequests(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Printf("%s %s %s", r.RemoteAddr, r.Method, r.URL.Path) + next.ServeHTTP(w, r) + }) +} + +func corsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + + next.ServeHTTP(w, r) + }) +} + +// ping godoc +// +// @Summary Check server health +// @Description Returns a pong response if the server is running +// @Tags health +// @Success 200 {string} string "pong" +// @Router /ping [get] +func (s *Server) ping(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "pong") +} + +// login godoc +// +// @Summary Redirect to login page +// @Description Redirects users to an external authentication page +// @Tags auth +// @Success 307 {string} string "Temporary Redirect" +// @Router /login [get] +func (s *Server) login(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "https://proxy.illegalesachen.download/login", http.StatusTemporaryRedirect) +} + +// song godoc +// +// @Summary Get a song by its hash +// @Description Retrieves a song using its unique hash identifier. +// @Tags songs +// @Accept json +// @Produce json +// @Param hash path string true "Song hash" +// @Success 200 {object} Song +// @Failure 400 {string} string "Invalid parameter" +// @Failure 404 {string} string "Song not found" +// @Router /song/{hash} [get] +func (s *Server) song(w http.ResponseWriter, r *http.Request) { + + hash := r.PathValue("hash") + if hash == "" { + http.Error(w, ErrRequiredParameterNotPresent.Error(), http.StatusBadRequest) + return + } + + song, err := getSong(s.Db, hash) + if err != nil { + fmt.Println(err) + http.Error(w, "beatmap not found by hash", http.StatusNotFound) + return + } + + writeJSON(w, song, http.StatusOK) +} + +// recents godoc +// +// @Summary Get a list of recent songs +// @Description Retrieves recent songs with pagination support. +// @Tags songs +// @Accept json +// @Produce json +// @Param limit query int false "Limit" default(10) +// @Param offset query int false "Offset" default(0) +// @Success 200 {array} Song +// @Failure 500 {string} string "Internal server error" +// @Router /songs/recents [get] +func (s *Server) recents(w http.ResponseWriter, r *http.Request) { + + limit, offset := pagination(r) + recent, err := getRecent(s.Db, limit, offset) + if err != nil { + fmt.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, recent, http.StatusOK) +} + +// favorites godoc +// +// @Summary Get a list of favorite songs based on a query +// @Description Retrieves favorite songs filtered by a query with pagination support. +// @Tags songs +// @Accept json +// @Produce json +// @Param query query string true "Search query" +// @Param limit query int false "Limit" default(10) +// @Param offset query int false "Offset" default(0) +// @Success 200 {array} Song +// @Failure 400 {string} string "Invalid parameter" +// @Failure 500 {string} string "Internal server error" +// @Router /songs/favorites [get] +func (s *Server) favorites(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query().Get("query") + if query == "" { + http.Error(w, ErrRequiredParameterNotPresent.Error(), http.StatusBadRequest) + return + } + + limit, offset := pagination(r) + + favorites, err := getFavorites(s.Db, query, limit, offset) + if err != nil { + fmt.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, favorites, http.StatusOK) +} + +// collection godoc +// +// @Summary Get a collection of songs by index +// @Description Retrieves a collection of songs using the provided index. +// @Tags songs +// @Accept json +// @Produce json +// @Param index query int false "Index" +// @Param name query string false "Index" +// @Success 200 {array} Song +// @Failure 400 {string} string "Invalid parameter" +// @Failure 500 {string} string "Internal server error" +// @Router /collection [get] +func (s *Server) collection(w http.ResponseWriter, r *http.Request) { + limit, offset := pagination(r) + name := r.URL.Query().Get("name") + if name != "" { + c, err := getCollectionByName(s.Db, limit, offset, name) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, c, http.StatusOK) + return + } + + index, err := strconv.Atoi(r.URL.Query().Get("index")) + if err != nil { + http.Error(w, ErrRequiredParameterNotPresent.Error(), http.StatusBadRequest) + return + } else { + c, err := getCollection(s.Db, limit, offset, index) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, c, http.StatusOK) + return + } + +} + +// @Summary Searches collections based on a query +// @Description Searches collections in the database based on the query parameter +// @Tags search +// @Accept json +// @Produce json +// @Param query query string true "Search query" +// @Param limit query int false "Limit the number of results" default(10) +// @Param offset query int false "Offset for pagination" default(0) +// @Success 200 {array} Collection "List of collections" +// @Failure 400 {object} string "Bad Request" +// @Failure 500 {object} string "Internal Server Error" +// @Router /search/collections [get] +func (s *Server) collectionSearch(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query().Get("query") + + limit, offset := pagination(r) + + preview, err := getCollections(s.Db, q, limit, offset) + if err != nil { + fmt.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, preview, http.StatusOK) +} + +// @Summary Returns all the Songs of a specific Artist +// @Tags songs +// @Accept json +// @Produce json +// @Param artist query string true "Artist Name" +// @Success 200 {array} Song +// @Failure 400 {object} string "Bad Request" +// @Failure 500 {object} string "Internal Server Error" +// @Router /songs/artist [get] +func (s *Server) aristsSongs(w http.ResponseWriter, r *http.Request) { + artist := r.URL.Query().Get("artist") + if artist == "" { + http.Error(w, ErrRequiredParameterNotPresent.Error(), http.StatusBadRequest) + return + } + + writeJSON(w, []Song{}, http.StatusOK) +} + +// @Summary Searches active records based on a query +// @Description Searches active records in the database based on the query parameter +// @Tags search +// @Accept json +// @Produce json +// @Param query query string true "Search query" +// @Param limit query int false "Limit the number of results" default(10) +// @Param offset query int false "Offset for pagination" default(0) +// @Success 200 {object} ActiveSearch "Active search result" +// @Failure 400 {object} string "Bad Request" +// @Failure 500 {object} string "Internal Server Error" +// @Router /search/active [get] +func (s *Server) activeSearch(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query().Get("query") + if q == "" { + http.Error(w, ErrRequiredParameterNotPresent.Error(), http.StatusBadRequest) + return + } + + //TODO + limit, offset := pagination(r) + + recent, err := getSearch(s.Db, q, limit, offset) + if err != nil { + fmt.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, recent, http.StatusOK) +} + +// @Summary Searches for artists based on a query +// @Description Searches for artists in the database based on the query parameter +// @Tags search +// @Accept json +// @Produce json +// @Param query query string true "Search query" +// @Param limit query int false "Limit the number of results" default(10) +// @Param offset query int false "Offset for pagination" default(0) +// @Success 200 {object} Artist "List of artists" +// @Failure 400 {object} string "Bad Request" +// @Failure 500 {object} string "Internal Server Error" +// @Router /search/artist [get] +func (s *Server) artistSearch(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query().Get("query") + if q == "" { + http.Error(w, ErrRequiredParameterNotPresent.Error(), http.StatusBadRequest) + return + } + + //TODO + limit, offset := pagination(r) + + a, err := getArtists(s.Db, q, limit, offset) + if err != nil { + fmt.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, a, http.StatusOK) +} + +// @Summary Retrieves a song file by its encoded path +// @Description Retrieves a song file from the server based on the provided encoded filepath +// @Tags files +// @Accept json +// @Produce json +// @Param filepath path string true "Base64 encoded file path" +// @Success 200 {file} File "The requested song file" +// @Failure 400 {object} string "Bad Request" +// @Failure 404 {object} string "File Not Found" +// @Router /audio/{filepath} [get] +func (s *Server) songFile(w http.ResponseWriter, r *http.Request) { + f := r.PathValue("filepath") + if f == "" { + http.Error(w, ErrRequiredParameterNotPresent.Error(), http.StatusBadRequest) + return + } + + filename, err := base64.RawStdEncoding.DecodeString(f) + if err != nil { + fmt.Println(err) + http.Error(w, ErrRequiredParameterNotPresent.Error(), http.StatusBadRequest) + return + } + + file, err := os.Open(s.OsuDir + "Songs/" + string(filename)) + if err != nil { + fmt.Println(err) + http.Error(w, ErrFileNotFound.Error(), http.StatusNotFound) + return + } + defer file.Close() + + stat, err := file.Stat() + if err != nil { + fmt.Println(err) + http.Error(w, ErrFileNotFound.Error(), http.StatusNotFound) + return + } + + http.ServeContent(w, r, stat.Name(), stat.ModTime(), file) +} + +// @Summary Retrieves an image file by its encoded path +// @Description Retrieves an image file from the server based on the provided encoded filepath +// @Tags files +// @Accept json +// @Produce json +// @Param filepath path string true "Base64 encoded file path" +// @Success 200 {file} File "The requested image file" +// @Failure 400 {object} string "Bad Request" +// @Failure 404 {object} string "File Not Found" +// @Router /image/{filepath} [get] +func (s *Server) imageFile(w http.ResponseWriter, r *http.Request) { + f := r.PathValue("filepath") + if f == "" { + http.Error(w, ErrRequiredParameterNotPresent.Error(), http.StatusBadRequest) + return + } + + filename, err := base64.RawStdEncoding.DecodeString(f) + if err != nil { + fmt.Println(err) + http.Error(w, ErrFailedToParseEncoded.Error(), http.StatusBadRequest) + return + } + + http.ServeFile(w, r, s.OsuDir+"Songs/"+string(filename)) +} + +func (s *Server) callback(w http.ResponseWriter, r *http.Request) { + cookie := r.URL.Query().Get("COOKIE") + + if cookie != "" { + s.Env["COOKIE"] = cookie + godotenv.Write(s.Env, ".env") + } +} + +func writeJSON(w http.ResponseWriter, v any, status int) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if err := json.NewEncoder(w).Encode(v); err != nil { + fmt.Println(err) + http.Error(w, "Failed to encode JSON response", http.StatusInternalServerError) + } +} + +func pagination(r *http.Request) (int, int) { + limit, err := strconv.Atoi(r.URL.Query().Get("limit")) + if err != nil || limit <= 0 || limit > 100 { + limit = 100 + } + offset, err := strconv.Atoi(r.URL.Query().Get("offset")) + if err != nil || offset < 0 { + offset = 0 + } + + return limit, offset +} diff --git a/grpc-backend/main.go b/grpc-backend/main.go new file mode 100644 index 0000000..c9f4b00 --- /dev/null +++ b/grpc-backend/main.go @@ -0,0 +1,232 @@ +package main + +import ( + v1 "backend/gen" + "bufio" + "bytes" + "context" + "encoding/json" + "fmt" + "log" + "net" + "net/http" + "os" + "os/exec" + "os/signal" + "path" + "regexp" + "strings" + "syscall" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/joho/godotenv" + "github.com/juli0n21/go-osu-parser/parser" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +func main() { + envMap, err := godotenv.Read(".env") + if err != nil { + fmt.Println("Error reading .env file") + } + + if envMap["OSU_PATH"] == "" { + fmt.Println("Osu Path not found! Please paste the full path to your osu! folder.") + fmt.Println("Osu Path not found pls paste the full Path to ur osu! folder \n it should start with 'C://' and can be opened from the Settings menu!)\n path: ") + + fmt.Scanln(&osuRoot) + osuRoot = strings.TrimSpace(osuRoot) + + envMap["OSU_PATH"] = osuRoot + godotenv.Write(envMap, ".env") + } + + osuRoot := envMap["OSU_PATH"] + cookie := envMap["COOKIE"] + port := GetEnv(envMap["PORT"], ":8080") + filename := path.Join(osuRoot, "osu!.db") + + osuDb, err := parser.ParseOsuDB(filename) + if err != nil { + log.Fatal(err) + } + + if cookie == "" { + fmt.Println("No Authentication found please follow the link to log in!\n http://proxy.illegalesachen.download/login") + } + + url, err := StartCloudflared(port) + if err != nil { + log.Fatalf("Cloudflared service couldnt be started: %v", err) + } + + if err = sendUrl(url, cookie); err != nil { + log.Fatalf("Couldnt Update Endpoint url with Proxy: %v", err) + } + + db, err := initDB("./data/music.db", osuDb, osuRoot) + if err != nil { + log.Fatal(err) + } + + s := &Server{ + Port: port, + Db: db, + OsuDir: osuRoot, + Env: envMap, + } + + // Run gRPC + grpc-gateway servers + if err := runGrpcAndGateway(s, port); err != nil { + log.Fatalf("Failed to run servers: %v", err) + } +} + +func GetEnv(key, fallback string) string { + if value, ok := os.LookupEnv(key); ok && value != "" { + return value + } + + return fallback +} + +func StartCloudflared(port string) (string, error) { + cmd := exec.Command("cloudflared", "tunnel", "--url", fmt.Sprintf("http://localhost%s", port)) + + stderr, err := cmd.StderrPipe() + if err != nil { + return "", fmt.Errorf("Error creating StderrPipe: %v", err) + + } + + if err := cmd.Start(); err != nil { + return "", fmt.Errorf("Error starting command: %v", err) + + } + + stderrScanner := bufio.NewScanner(stderr) + + urlRegex := regexp.MustCompile(`https?://[\w.-]+\.trycloudflare\.com`) + + for stderrScanner.Scan() { + line := stderrScanner.Text() + if url := urlRegex.FindString(line); url != "" { + fmt.Println("Found URL:", url) + return url, nil + } + } + + if err := cmd.Wait(); err != nil { + return "", fmt.Errorf("Error waiting for command: %v", err) + } + + if err := stderrScanner.Err(); err != nil { + return "", fmt.Errorf("Error reading stderr: %v", err) + } + + return "", fmt.Errorf("no url found") +} + +func sendUrl(endpoint, cookie string) error { + url := GetEnv("PROXY_URL", "https://proxy.illegalesachen.download/settings") + + payload := struct { + Sharing *bool `json:"sharing"` + Endpoint string `json:"endpoint"` + }{ + Endpoint: endpoint, + } + + body, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("Error marshalling payload: %v", err) + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) + if err != nil { + return fmt.Errorf("failed to create request: %v", err) + } + req.Header.Add("Content-Type", "application/json") + + req.AddCookie(&http.Cookie{ + Name: "session_cookie", + Value: cookie, + }) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("Error sending request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("Error in request: %s", resp.Status) + } + return nil +} + +func runGrpcAndGateway(s *Server, port string) error { + grpcPort := ":9090" // gRPC server port + httpPort := port // REST gateway port (e.g. ":8080") + + grpcLis, err := net.Listen("tcp", grpcPort) + if err != nil { + return fmt.Errorf("failed to listen on %s: %w", grpcPort, err) + } + grpcServer := grpc.NewServer() + v1.RegisterMusicBackendServer(grpcServer, s) // Register your service implementation + + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + gwMux := runtime.NewServeMux() + opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} + + err = v1.RegisterMusicBackendHandlerFromEndpoint(ctx, gwMux, grpcPort, opts) + if err != nil { + return fmt.Errorf("failed to register grpc-gateway: %w", err) + } + + mux := http.NewServeMux() + + mux.Handle("/api/v1/", gwMux) + + mux.HandleFunc("/files", s.songFile) + + fileServer := http.FileServer(http.Dir("gen/swagger")) + mux.Handle("/swagger/", http.StripPrefix("/swagger/", fileServer)) + + httpServer := &http.Server{ + Addr: httpPort, + Handler: mux, + } + + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, syscall.SIGTERM) + + errChan := make(chan error, 2) + + go func() { + log.Printf("Starting gRPC server on %s", grpcPort) + errChan <- grpcServer.Serve(grpcLis) + }() + + go func() { + log.Printf("Starting HTTP gateway server on %s", httpPort) + errChan <- httpServer.ListenAndServe() + }() + + select { + case <-stop: + log.Println("Shutting down servers...") + grpcServer.GracefulStop() + httpServer.Shutdown(ctx) + return nil + case err := <-errChan: + return err + } +} diff --git a/grpc-backend/models.go b/grpc-backend/models.go new file mode 100644 index 0000000..41fb4a9 --- /dev/null +++ b/grpc-backend/models.go @@ -0,0 +1,46 @@ +package main + +// Song represents a song entity +// @Description Song represents a song with metadata +type Song struct { + BeatmapID int `json:"beatmap_id" example:"123456"` + MD5Hash string `json:"md5_hash" example:"abcd1234efgh5678"` + Title string `json:"title" example:"Shape of You"` + Artist string `json:"artist" example:"Ed Sheeran"` + Creator string `json:"creator" example:"JohnDoe"` + Folder string `json:"folder" example:"osu/Songs/123456"` + File string `json:"file" example:"beatmap.osu"` + Audio string `json:"audio" example:"audio.mp3"` + TotalTime int64 `json:"total_time" example:"240"` + Image string `json:"image" example:"cover.jpg"` +} + +// CollectionPreview represents a preview of a song collection +// @Description CollectionPreview contains summary data of a song collection +type CollectionPreview struct { + Name string `json:"name" example:"Collection Name"` + Image string `json:"image" example:"cover.jpg"` + Items int `json:"items" example:"10"` +} + +// Collection represents a full song collection +// @Description Collection holds a list of songs +type Collection struct { + Name string `json:"name" example:"Best of 2023"` + Items int `json:"items" example:"15"` + Songs []Song `json:"songs"` +} + +// ActiveSearch represents an active song search query +// @Description ActiveSearch holds search results for a given artist +type ActiveSearch struct { + Artist string `json:"artist" example:"Ed Sheeran"` + Songs []Song `json:"songs"` +} + +// Artist represents an active song search query +// @Description Artist holds search results for a given artist +type Artist struct { + Artist string `json:"artist" example:"Miku"` + Count int `json:"count" example:"21"` +} diff --git a/grpc-backend/proto/osu_music.proto b/grpc-backend/proto/osu_music.proto new file mode 100644 index 0000000..17b822b --- /dev/null +++ b/grpc-backend/proto/osu_music.proto @@ -0,0 +1,162 @@ +syntax = "proto3"; + +package osu.music.api.v1; + +option go_package = "github.com/juli0n21/osu-music/api/v1"; + +import "google/api/annotations.proto"; + +service MusicBackend { + rpc Collections(CollectionRequest) returns (CollectionResponse) { + option (google.api.http) = { + get: "/v1/collections" + }; + } + rpc Song(SongRequest) returns (SongResponse){ + option (google.api.http) = { + get: "/v1/song/{hash}" + }; + } + rpc Artist(ArtistRequest) returns (ArtistResponse){ + option (google.api.http) = { + get: "/v1/artist/{artist}" + }; + } + rpc Favorite(FavoriteRequest) returns (FavoriteResponse){ + option (google.api.http) = { + get: "/v1/favorites" + }; + } + rpc Recent(RecentRequest) returns (RecentResponse){ + option (google.api.http) = { + get: "/v1/recent" + }; + } + + rpc Search(SearchSharedRequest) returns (SearchSharedResponse) { + option (google.api.http) = { + get: "/v1/search" + }; + } + rpc SearchArtists(SearchArtistRequest) returns (SearchArtistResponse) { + option (google.api.http) = { + get: "/v1/search/artists" + }; + } + rpc SearchCollections(SearchCollectionRequest) returns (SearchCollectionResponse) { + option (google.api.http) = { + get: "/v1/search/collections" + }; + } + rpc Ping(PingRequest) returns (PingResponse) { + option (google.api.http) = { + get: "/ping" + }; + } +} + +//===== songs ===== +message CollectionRequest { + string name = 1; + int32 index = 2; +} + +message CollectionResponse { + string name = 1; + int32 items = 2; + repeated Song songs = 3; +} + + +message SongRequest { + string hash = 1; +} + +message SongResponse { + repeated Song songs = 1; +} + +message ArtistRequest { + string artist = 1; +} + +message ArtistResponse { + repeated Song songs = 1; +} + +message FavoriteRequest { + string query = 1; + int32 limit = 2; + int32 offset = 3; +} + +message FavoriteResponse { + repeated Song songs = 1; +} + +message RecentRequest { + int32 limit = 2; + int32 offset = 3; +} + +message RecentResponse { + repeated Song songs = 1; +} + +//===== search ===== +message SearchSharedRequest { + string query = 1; + int32 limit = 2; + int32 offset = 3; +} + +message SearchSharedResponse { + string artist = 1; + repeated Song songs = 2; +} + +message SearchArtistRequest { + string query = 1; + int32 limit = 2; + int32 offset = 3; +} + +message SearchArtistResponse { + string artist = 1; + int32 items = 2; +} + +message SearchCollectionRequest { + string query = 1; + int32 limit = 2; + int32 offset = 3; +} + +message SearchCollectionResponse { + string name = 1; + string image = 2; + int32 items = 3; +} + +//===== status ===== +message PingRequest { + string ping = 1; +} + +message PingResponse { + string pong = 1; +} + +//===== models ===== +message Song { + int32 beatmap_id = 1; + string md5_hash = 2; + string title = 3; + string artist = 4; + string creator = 5; + string folder = 6; + string file = 7; + string audio = 8; + int64 total_time = 9; + string image = 10; +} diff --git a/osu_music.proto b/osu_music.proto new file mode 100644 index 0000000..c0f1beb --- /dev/null +++ b/osu_music.proto @@ -0,0 +1,162 @@ +syntax = "proto3"; + +package osu.music.api.v1; + +option go_package = "github.com/juli0n21/osu-music/api/v1"; + +import "google/api/annotations.proto"; + +service MusicBackend { + rpc Collections(CollectionRequest) returns (CollectionResponse) { + option (google.api.http) = { + get: "/v1/collections" + }; + } + rpc Song(SongRequest) returns (SongResponse){ + option (google.api.http) = { + get: "/v1/song/{hash}" + }; + } + rpc Artist(ArtistRequest) returns (ArtistResponse){ + option (google.api.http) = { + get: "/v1/artist{artist}" + }; + } + rpc Favorite(FavoriteRequest) returns (FavoriteResponse){ + option (google.api.http) = { + get: "/v1/favorites" + }; + } + rpc Recent(RecentRequest) returns (RecentResponse){ + option (google.api.http) = { + get: "/v1/recent" + }; + } + + rpc Search(SearchSharedRequest) returns (SearchSharedResponse) { + option (google.api.http) = { + get: "/v1/search" + }; + } + rpc SearchArtists(SearchArtistRequest) returns (SearchArtistResponse) { + option (google.api.http) = { + get: "/v1/search/artists" + }; + } + rpc SearchCollections(SearchCollectionRequest) returns (SearchCollectionResponse) { + option (google.api.http) = { + get: "/v1/search/collections" + }; + } + rpc Ping(PingRequest) returns (PingResponse) { + option (google.api.http) = { + get: "/ping" + }; + } +} + +//===== songs ===== +message CollectionRequest { + string name = 1; + int32 index = 2; +} + +message CollectionResponse { + string name = 1; + int32 items = 2; + repeated Song songs = 3; +} + + +message SongRequest { + string hash = 1; +} + +message SongResponse { + repeated Song songs = 1; +} + +message ArtistRequest { + string artist = 1; +} + +message ArtistResponse { + repeated Song songs = 1; +} + +message FavoriteRequest { + string query = 1; + int32 limit = 2; + int32 offset = 3; +} + +message FavoriteResponse { + repeated Song songs = 1; +} + +message RecentRequest { + int32 limit = 2; + int32 offset = 3; +} + +message RecentResponse { + repeated Song songs = 1; +} + +//===== search ===== +message SearchSharedRequest { + string query = 1; + int32 limit = 2; + int32 offset = 3; +} + +message SearchSharedResponse { + string artist = 1; + repeated Song = 2; +} + +message SearchArtistRequest { + string query = 1; + int32 limit = 2; + int32 offset = 3; +} + +message SearchArtistResponse { + string artist = 1; + int32 items = 2; +} + +message SearchCollectionRequest { + string query = 1; + int32 limit = 2; + int32 offset = 3; +} + +message SearchCollectionResponse { + string name = 1; + string image = 2; + int32 items = 3; +} + +//===== status ===== +message PingRequest { + string ping = 1; +} + +message PingResponse { + string pong = 1; +} + +//===== models ===== +message Song { + int32 beatmap_id = 1; + string md5_hash = 2; + string title = 3; + string artist = 4; + string creator = 5; + string folder = 6; + string file = 7; + string audio = 8; + int64 total_time = 9; + string image = 10; +} diff --git a/proxy/auth.go b/proxy/auth.go index 5b26cf4..cfff527 100644 --- a/proxy/auth.go +++ b/proxy/auth.go @@ -251,7 +251,7 @@ func Oauth(w http.ResponseWriter, r *http.Request) { var copyText = document.getElementById("myInput"); copyText.select(); copyText.setSelectionRange(0, 99999); // For mobile devices - navigator.clipboard.writeText(copyText.value); + navigator.clipboard.writeText(copyText.value); } window.close(); // Close the window after copy diff --git a/proxy/go.mod b/proxy/go.mod index 444aa07..ea1e6d5 100644 --- a/proxy/go.mod +++ b/proxy/go.mod @@ -1,6 +1,6 @@ module proxy -go 1.22.1 +go 1.24.3 require ( github.com/joho/godotenv v1.5.1 diff --git a/proxy/routes.go b/proxy/routes.go index 754a0d6..c1eb92c 100644 --- a/proxy/routes.go +++ b/proxy/routes.go @@ -17,8 +17,6 @@ func run() error { mux.Handle("GET /oauth/code", http.HandlerFunc(Oauth)) mux.Handle("POST /settings", AuthMiddleware(http.HandlerFunc(Settings))) - // mux.Handle("POST /setting", ); - fmt.Println("Starting Server on", port) //global middleware @@ -77,7 +75,6 @@ func Settings(w http.ResponseWriter, r *http.Request) { } w.WriteHeader(http.StatusOK) - return } func JSONResponse(w http.ResponseWriter, statusCode int, data interface{}) {