actual changes :3

This commit is contained in:
2025-05-21 01:20:56 +02:00
parent 005ae783c5
commit 396ccc28f4
22 changed files with 3710 additions and 36 deletions

View File

@@ -669,8 +669,8 @@ const docTemplate = `{
// SwaggerInfo holds exported Swagger Info so clients can modify it // SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{ var SwaggerInfo = &swag.Spec{
Version: "1.0", Version: "1.0",
Host: "localhost:8080", Host: "",
BasePath: "/api/v1/", BasePath: "api/v1/",
Schemes: []string{}, Schemes: []string{},
Title: "go-osu-music-hoster", Title: "go-osu-music-hoster",
Description: "Server Hosting ur own osu files over a simple Api", Description: "Server Hosting ur own osu files over a simple Api",

View File

@@ -6,7 +6,7 @@
"contact": {}, "contact": {},
"version": "1.0" "version": "1.0"
}, },
"host": "localhost:8080", "host": "/",
"basePath": "/api/v1/", "basePath": "/api/v1/",
"paths": { "paths": {
"/audio/{filepath}": { "/audio/{filepath}": {

View File

@@ -69,7 +69,7 @@ definitions:
example: 240 example: 240
type: integer type: integer
type: object type: object
host: localhost:8080 host: /
info: info:
contact: {} contact: {}
description: Server Hosting ur own osu files over a simple Api description: Server Hosting ur own osu files over a simple Api

View File

@@ -1,6 +1,6 @@
module backend module backend
go 1.23.5 go 1.24.3
require ( require (
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1

View File

@@ -31,32 +31,58 @@ type Server struct {
Env map[string]string Env map[string]string
} }
func (s *Server) registerRoutes() { func (s *Server) registerRoutes() http.Handler {
http.HandleFunc("/api/v1/ping", s.ping) mux := http.NewServeMux()
http.HandleFunc("/api/v1/login", s.login)
http.HandleFunc("/api/v1/song/{hash}/", s.song) mux.HandleFunc("/api/v1/ping", s.ping)
http.HandleFunc("/api/v1/songs/recents", s.recents) mux.HandleFunc("/api/v1/login", s.login)
http.HandleFunc("/api/v1/songs/favorites", s.favorites)
http.HandleFunc("/api/v1/songs/artist", s.aristsSongs)
http.HandleFunc("/api/v1/collection", s.collection) mux.HandleFunc("/api/v1/song/{hash}/", s.song)
http.HandleFunc("/api/v1/search/collections", s.collectionSearch) 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) mux.HandleFunc("/api/v1/collection", s.collection)
http.HandleFunc("/api/v1/search/artist", s.artistSearch) mux.HandleFunc("/api/v1/search/collections", s.collectionSearch)
http.HandleFunc("/api/v1/audio/{filepath}", s.songFile) mux.HandleFunc("/api/v1/search/active", s.activeSearch)
http.HandleFunc("/api/v1/image/{filepath}", s.imageFile) mux.HandleFunc("/api/v1/search/artist", s.artistSearch)
http.HandleFunc("/api/v1/callback", s.callback) mux.HandleFunc("/api/v1/audio/{filepath}", s.songFile)
http.Handle("/swagger/", httpSwagger.WrapHandler) 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) { func run(s *Server) {
s.registerRoutes() mux := s.registerRoutes()
fmt.Println("starting server on http://localhost" + s.Port) 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 // ping godoc

View File

@@ -21,10 +21,9 @@ import (
// @version 1.0 // @version 1.0
// @description Server Hosting ur own osu files over a simple Api // @description Server Hosting ur own osu files over a simple Api
// @host localhost:8080 // @host /
// @BasePath /api/v1/ // @BasePath /api/v1/
func main() { func main() {
envMap, err := godotenv.Read(".env") envMap, err := godotenv.Read(".env")
if err != nil { if err != nil {
fmt.Println("Error reading .env file") fmt.Println("Error reading .env file")
@@ -125,7 +124,7 @@ func StartCloudflared(port string) (string, error) {
} }
func sendUrl(endpoint, cookie 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 { payload := struct {
Sharing *bool `json:"sharing"` Sharing *bool `json:"sharing"`
@@ -137,22 +136,19 @@ func sendUrl(endpoint, cookie string) error {
body, err := json.Marshal(payload) body, err := json.Marshal(payload)
if err != nil { if err != nil {
return fmt.Errorf("Error marshalling payload: %v", err) return fmt.Errorf("Error marshalling payload: %v", err)
} }
req, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) req, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
if err != nil { 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{ req.AddCookie(&http.Cookie{
Name: "session_cookie", Name: "session_cookie",
Value: cookie, Value: cookie,
}) })
req.Header.Set("Content-Type", "application/json")
fmt.Println(req)
client := &http.Client{} client := &http.Client{}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
@@ -163,6 +159,5 @@ func sendUrl(endpoint, cookie string) error {
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return fmt.Errorf("Error in request: %s", resp.Status) return fmt.Errorf("Error in request: %s", resp.Status)
} }
fmt.Println("Response Status:", resp.Status)
return nil return nil
} }

3
grpc-backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
data/
gen/
*.env

30
grpc-backend/Makefile Normal file
View File

@@ -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

599
grpc-backend/database.go Normal file
View File

@@ -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
}

683
grpc-backend/docs/docs.go Normal file
View File

@@ -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)
}

View File

@@ -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
}
}
}
}
}

View File

@@ -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"

44
grpc-backend/go.mod Normal file
View File

@@ -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
)

152
grpc-backend/go.sum Normal file
View File

@@ -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=

446
grpc-backend/handlers.go Normal file
View File

@@ -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
}

232
grpc-backend/main.go Normal file
View File

@@ -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
}
}

46
grpc-backend/models.go Normal file
View File

@@ -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"`
}

View File

@@ -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;
}

162
osu_music.proto Normal file
View File

@@ -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;
}

View File

@@ -1,6 +1,6 @@
module proxy module proxy
go 1.22.1 go 1.24.3
require ( require (
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1

View File

@@ -17,8 +17,6 @@ func run() error {
mux.Handle("GET /oauth/code", http.HandlerFunc(Oauth)) mux.Handle("GET /oauth/code", http.HandlerFunc(Oauth))
mux.Handle("POST /settings", AuthMiddleware(http.HandlerFunc(Settings))) mux.Handle("POST /settings", AuthMiddleware(http.HandlerFunc(Settings)))
// mux.Handle("POST /setting", );
fmt.Println("Starting Server on", port) fmt.Println("Starting Server on", port)
//global middleware //global middleware
@@ -77,7 +75,6 @@ func Settings(w http.ResponseWriter, r *http.Request) {
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
return
} }
func JSONResponse(w http.ResponseWriter, statusCode int, data interface{}) { func JSONResponse(w http.ResponseWriter, statusCode int, data interface{}) {