mirror of
https://github.com/JuLi0n21/pwa-player.git
synced 2026-04-19 15:30:05 +00:00
actual changes :3
This commit is contained in:
3
grpc-backend/.gitignore
vendored
Normal file
3
grpc-backend/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
data/
|
||||
gen/
|
||||
*.env
|
||||
30
grpc-backend/Makefile
Normal file
30
grpc-backend/Makefile
Normal 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
599
grpc-backend/database.go
Normal 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
683
grpc-backend/docs/docs.go
Normal 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)
|
||||
}
|
||||
656
grpc-backend/docs/swagger.json
Normal file
656
grpc-backend/docs/swagger.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
441
grpc-backend/docs/swagger.yaml
Normal file
441
grpc-backend/docs/swagger.yaml
Normal 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
44
grpc-backend/go.mod
Normal 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
152
grpc-backend/go.sum
Normal 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
446
grpc-backend/handlers.go
Normal 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
232
grpc-backend/main.go
Normal 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
46
grpc-backend/models.go
Normal 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"`
|
||||
}
|
||||
162
grpc-backend/proto/osu_music.proto
Normal file
162
grpc-backend/proto/osu_music.proto
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user