replace sql mostly with sqlc

This commit is contained in:
2025-07-16 22:10:49 +02:00
parent a3440326e8
commit 7c29f64f6d
14 changed files with 418 additions and 741 deletions

View File

@@ -10,7 +10,6 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"strconv"
"github.com/juli0n21/go-osu-parser/parser" "github.com/juli0n21/go-osu-parser/parser"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
@@ -49,7 +48,9 @@ func initDB(connectionString string, osuDb *parser.OsuDB, osuroot string) (*sql.
return nil, nil, err return nil, nil, err
} }
if err = checkhealth(db, osuDB); err != nil { sqlcQueries := sqlcdb.New(db)
if err = checkhealth(sqlcQueries, osuDB); err != nil {
if err = rebuildBeatmapDb(db, osuDB); err != nil { if err = rebuildBeatmapDb(db, osuDB); err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -70,8 +71,6 @@ func initDB(connectionString string, osuDb *parser.OsuDB, osuroot string) (*sql.
} }
} }
sqlcQueries := sqlcdb.New(db)
return db, sqlcQueries, nil return db, sqlcQueries, nil
} }
@@ -90,13 +89,13 @@ func createDB(db *sql.DB) error {
MD5Hash TEXT DEFAULT '00000000000000000000000000000000', MD5Hash TEXT DEFAULT '00000000000000000000000000000000',
File TEXT DEFAULT 'unknown.osu', File TEXT DEFAULT 'unknown.osu',
RankedStatus TEXT DEFAULT Unknown, RankedStatus TEXT DEFAULT Unknown,
LastModifiedTime DATETIME DEFAULT '0001-01-01 00:00:00', LastModifiedTime INTEGER DEFAULT 0,
TotalTime INTEGER DEFAULT 0, TotalTime INTEGER DEFAULT 0,
AudioPreviewTime INTEGER DEFAULT 0, AudioPreviewTime INTEGER DEFAULT 0,
BeatmapSetId INTEGER DEFAULT -1, BeatmapSetId INTEGER DEFAULT -1,
Source TEXT DEFAULT '', Source TEXT DEFAULT '',
Tags TEXT DEFAULT '', Tags TEXT DEFAULT '',
LastPlayed DATETIME DEFAULT '0001-01-01 00:00:00', LastPlayed INTEGER DEFAULT 0,
Folder TEXT DEFAULT 'Unknown Folder', Folder TEXT DEFAULT 'Unknown Folder',
UNIQUE (Artist, Title, MD5Hash, Difficulty) UNIQUE (Artist, Title, MD5Hash, Difficulty)
); );
@@ -146,34 +145,24 @@ func createCollectionDB(db *sql.DB) error {
return nil return nil
} }
func checkhealth(db *sql.DB, osuDb *parser.OsuDB) error { func checkhealth(db *sqlcdb.Queries, osuDb *parser.OsuDB) error {
rows, err := db.Query(`SELECT COUNT(*) FROM Beatmap GROUP BY BeatmapSetId;`) count, err := db.GetBeatmapSetCount(context.TODO())
if err != nil { if err != nil {
return err return err
} }
defer rows.Close()
var count int if count != int64(osuDb.FolderCount) {
if err = rows.Scan(&count); err != nil {
return err
}
if count != int(osuDb.FolderCount) {
log.Println("Folder count missmatch rebuilding db...") log.Println("Folder count missmatch rebuilding db...")
return ErrBeatmapCountNotMatch return ErrBeatmapCountNotMatch
} }
rows, err = db.Query(`SELECT COUNT(*) FROM Beatmap;`) count, err = db.GetBeatmapCount(context.TODO())
if err != nil { if err != nil {
return err return err
} }
if err = rows.Scan(&count); err != nil { if count != int64(osuDb.NumberOfBeatmaps) {
return err
}
if count != int(osuDb.NumberOfBeatmaps) {
log.Println("Beatmap count missmatch rebuilding db...") log.Println("Beatmap count missmatch rebuilding db...")
return ErrBeatmapCountNotMatch return ErrBeatmapCountNotMatch
} }
@@ -310,268 +299,6 @@ func rebuildCollectionDb(db *sql.DB, collectionDb *parser.Collections) error {
return tx.Commit() 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(ctx context.Context, q *sqlcdb.Queries, limit, offset int) ([]Song, error) {
rows, err := q.GetRecentBeatmaps(ctx, sqlcdb.GetRecentBeatmapsParams{
Limit: int64(limit), Offset: int64(offset),
})
if err != nil {
return nil, err
}
var songs []Song
for _, song := range rows {
songs = append(songs, convertToSong(song))
}
return songs, nil
}
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 Title LIKE ? OR Artist LIKE ? LIMIT ? OFFSET ?
`, "%"+q+"%", "%"+q+"%", limit, offset)
if err != nil {
return ActiveSearch{}, err
}
defer rows.Close()
s, err := scanSongs(rows)
if err != nil {
return ActiveSearch{}, err
}
return ActiveSearch{Songs: s}, 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) AS Count,
MIN(b.Folder) AS Folder,
MIN(b.File) AS 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 nil, err
}
bm, err := parser.ParseOsuFile(fmt.Sprintf("%sSongs/%s/%s", osuRoot, s.Folder, s.File))
if err != nil {
fmt.Println(err)
s.Image = "404.png"
} else {
if bgImage := bm.BackgroundImage(); bgImage != "" {
s.Image = fmt.Sprintf("%s/%s", s.Folder, bgImage)
} else {
s.Image = "404.png"
}
}
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 { func extractImageFromFile(osuRoot, folder, file string) string {
bm, err := parser.ParseOsuFile(filepath.Join(osuRoot, "Songs", folder, file)) bm, err := parser.ParseOsuFile(filepath.Join(osuRoot, "Songs", folder, file))
if err != nil { if err != nil {
@@ -586,67 +313,3 @@ func extractImageFromFile(osuRoot, folder, file string) string {
return "404.png" 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
}
func convertToSong(r sqlcdb.GetRecentBeatmapsRow) Song {
return Song{
BeatmapID: safeInt64(r.Beatmapid),
MD5Hash: safeString(r.Md5hash),
Title: safeString(r.Title),
Artist: safeString(r.Artist),
Creator: safeString(r.Creator),
Folder: safeString(r.Folder),
File: safeString(r.File),
Audio: safeString(r.Audio),
TotalTime: int64(safeInt64(r.Totaltime)),
Image: extractImageFromFile(osuRoot, safeString(r.Folder), safeString(r.File)),
}
}
func safeString(ns sql.NullString) string {
if ns.Valid {
return ns.String
}
return ""
}
func safeInt64(n sql.NullInt64) int {
if n.Valid {
return int(n.Int64)
}
return 0
}

View File

@@ -5,9 +5,9 @@ go 1.24.5
require ( require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/juli0n21/go-osu-parser v0.0.10 github.com/juli0n21/go-osu-parser v0.0.11
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79
google.golang.org/grpc v1.73.0 google.golang.org/grpc v1.74.0
google.golang.org/protobuf v1.36.6 google.golang.org/protobuf v1.36.6
modernc.org/sqlite v1.38.0 modernc.org/sqlite v1.38.0
) )
@@ -26,7 +26,7 @@ require (
golang.org/x/net v0.42.0 // indirect golang.org/x/net v0.42.0 // indirect
golang.org/x/sys v0.34.0 // indirect golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect golang.org/x/text v0.27.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79 // indirect
modernc.org/libc v1.66.3 // indirect modernc.org/libc v1.66.3 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect

View File

@@ -1,7 +1,7 @@
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 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/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.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 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-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -16,8 +16,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+u
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/juli0n21/go-osu-parser v0.0.9 h1:3hx+ZtLKRJGdyJLhRbZzGyJ60Q3yRk9+tDJqwBJOZEg= github.com/juli0n21/go-osu-parser v0.0.11 h1:p0kZc7zQ4YdmDO9/gor4zgvw2nfQPoDUtUXpV+hcOgU=
github.com/juli0n21/go-osu-parser v0.0.9/go.mod h1:oLLWnZReOMW4i5aNva/zvXsFqzdQigrbjyxOSs0cx+0= github.com/juli0n21/go-osu-parser v0.0.11/go.mod h1:oLLWnZReOMW4i5aNva/zvXsFqzdQigrbjyxOSs0cx+0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
@@ -26,16 +26,16 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 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/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc= golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc=
golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
@@ -51,12 +51,12 @@ golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU= google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79 h1:iOye66xuaAK0WnkPuhQPUFy8eJcmwUXqGGP3om6IxX8=
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA= google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79/go.mod h1:HKJDgKsFUnv5VAGeQjz8kxcgDP0HoE0iZNp0OdZNlhE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79 h1:1ZwqphdOdWYXsUHgMpU/101nCtf/kSp9hOrcvFsnl10=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/grpc v1.74.0 h1:sxRSkyLxlceWQiqDofxDot3d4u7DyoHPc7SBXMj8gGY=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/grpc v1.74.0/go.mod h1:NZUaK8dAMUfzhK6uxZ+9511LtOrk73UGWOFoNvz7z+s=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=

View File

@@ -12,6 +12,7 @@ import (
v1 "backend/gen" v1 "backend/gen"
"backend/internal/db" "backend/internal/db"
sqlcdb "backend/internal/db"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
@@ -83,36 +84,39 @@ func (s *Server) Song(ctx context.Context, req *v1.SongRequest) (*v1.SongRespons
return nil, status.Errorf(codes.InvalidArgument, "hash is required and cant be empty") return nil, status.Errorf(codes.InvalidArgument, "hash is required and cant be empty")
} }
song, err := getSong(s.Db, hash) song, err := s.Sqlc.GetBeatmapByHash(ctx, sql.NullString{Valid: true, String: hash})
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return nil, status.Errorf(codes.NotFound, "beatmap not found by hash") return nil, status.Errorf(codes.NotFound, "beatmap not found by hash")
} }
return &v1.SongResponse{ return &v1.SongResponse{
Song: song.toProto(), Song: toProtoSongSqlC(song),
}, nil }, nil
} }
func (s *Server) Recent(ctx context.Context, req *v1.RecentRequest) (*v1.RecentResponse, error) { func (s *Server) Recent(ctx context.Context, req *v1.RecentRequest) (*v1.RecentResponse, error) {
limit := defaultLimit(int(req.Limit)) limit := defaultLimit(int(req.Limit))
offset := int(req.Offset) offset := int(req.Offset)
recent, err := getRecent(context.Background(), s.Sqlc, limit, offset) rows, err := s.Sqlc.GetRecentBeatmaps(ctx, sqlcdb.GetRecentBeatmapsParams{
Limit: int64(limit),
Offset: int64(offset),
})
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return nil, status.Errorf(codes.Internal, "failed to get recents") return nil, status.Errorf(codes.Internal, "failed to get recents")
} }
return &v1.RecentResponse{ return &v1.RecentResponse{
Songs: toProtoSongs(recent), Songs: toProtoSongsSqlC(rows),
}, nil }, nil
} }
func (s *Server) Favorite(ctx context.Context, req *v1.FavoriteRequest) (*v1.FavoriteResponse, error) { func (s *Server) Favorite(ctx context.Context, req *v1.FavoriteRequest) (*v1.FavoriteResponse, error) {
return nil, fmt.Errorf("not implemented!")
limit := defaultLimit(int(req.Limit)) /*limit := defaultLimit(int(req.Limit))
offset := int(req.Offset) offset := int(req.Offset)
favorites, err := getFavorites(s.Db, req.Query, limit, offset) favorites, err := getFavorites(s.Db, req.Query, limit, offset)
if err != nil { if err != nil {
@@ -123,6 +127,7 @@ func (s *Server) Favorite(ctx context.Context, req *v1.FavoriteRequest) (*v1.Fav
return &v1.FavoriteResponse{ return &v1.FavoriteResponse{
Songs: toProtoSongs(favorites), Songs: toProtoSongs(favorites),
}, nil }, nil
*/
} }
func (s *Server) Collections(ctx context.Context, req *v1.CollectionRequest) (*v1.CollectionResponse, error) { func (s *Server) Collections(ctx context.Context, req *v1.CollectionRequest) (*v1.CollectionResponse, error) {
@@ -132,29 +137,32 @@ func (s *Server) Collections(ctx context.Context, req *v1.CollectionRequest) (*v
name := req.Name name := req.Name
if name != "" { if name != "" {
c, err := getCollectionByName(s.Db, limit, offset, name) c, err := s.Sqlc.GetCollectionByName(ctx,
sqlcdb.GetCollectionByNameParams{
Name: sql.NullString{Valid: true, String: name},
Limit: int64(limit),
Offset: int64(offset),
},
)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to fetch collection with name: %s", name)) return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to fetch collection with name: %s", name))
} }
return &v1.CollectionResponse{
Songs: toProtoSongs(c.Songs), return toProtoCollectionSqlc(c), nil
Items: int32(c.Items),
Name: c.Name,
}, nil
} }
fmt.Println(limit, offset, req.Index)
c, err := getCollection(s.Db, limit, offset, int(req.Index)) c, err := s.Sqlc.GetCollectionByOffset(ctx, sqlcdb.GetCollectionByOffsetParams{
Offset: int64(req.Index),
Limit: int64(limit),
Offset_2: int64(offset),
})
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to fetch collection with index: %d", req.Index)) return nil, status.Errorf(codes.Internal, fmt.Sprintf("failed to fetch collection with index: %d", req.Index))
} }
fmt.Println(c)
return &v1.CollectionResponse{ return toProtoCollectionoffsetSqlc(c), nil
Songs: toProtoSongs(c.Songs),
Items: int32(c.Items),
Name: c.Name,
}, nil
} }
@@ -163,38 +171,45 @@ func (s *Server) Search(ctx context.Context, req *v1.SearchSharedRequest) (*v1.S
if q == "" { if q == "" {
return nil, status.Error(codes.InvalidArgument, "query cant be empty") return nil, status.Error(codes.InvalidArgument, "query cant be empty")
} }
q = "%" + q + "%"
limit := defaultLimit(int(req.Limit)) limit := defaultLimit(int(req.Limit))
offset := int(req.Offset) offset := int(req.Offset)
search, err := getSearch(s.Db, q, limit, offset) search, err := s.Sqlc.SearchBeatmaps(ctx, sqlcdb.SearchBeatmapsParams{
Title: sql.NullString{String: q, Valid: true},
Artist: sql.NullString{String: q, Valid: true},
Limit: int64(limit),
Offset: int64(offset),
})
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return nil, status.Error(codes.Internal, "failed to fetch search") return nil, status.Error(codes.Internal, "failed to fetch search")
} }
return &v1.SearchSharedResponse{ return &v1.SearchSharedResponse{
Artist: search.Artist, Artist: "",
Songs: toProtoSongs(search.Songs), Songs: toProtoSongsSqlC(search),
}, nil }, nil
} }
func (s *Server) SearchCollections(ctx context.Context, req *v1.SearchCollectionRequest) (*v1.SearchCollectionResponse, error) { func (s *Server) SearchCollections(ctx context.Context, req *v1.SearchCollectionRequest) (*v1.SearchCollectionResponse, error) {
q := req.Query q := req.Query
q = "%" + q + "%"
limit := defaultLimit(int(req.Limit)) //limit := defaultLimit(int(req.Limit))
limit := 10000
offset := int(req.Offset) offset := int(req.Offset)
fmt.Println(req) preview, err := s.Sqlc.SearchCollection(ctx, sqlcdb.SearchCollectionParams{
preview, err := getCollections(s.Db, q, limit, offset) Name: sql.NullString{String: q, Valid: true},
Limit: int64(limit),
Offset: int64(offset),
})
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return nil, status.Errorf(codes.Internal, "failed to search for collections") return nil, status.Errorf(codes.Internal, "failed to search for collections")
} }
return toProtoCollectionsSearchPreview(preview), nil
return &v1.SearchCollectionResponse{
Collections: toProtoCollectionPreview(preview),
}, nil
} }
func (s *Server) SearchArtists(ctx context.Context, req *v1.SearchArtistRequest) (*v1.SearchArtistResponse, error) { func (s *Server) SearchArtists(ctx context.Context, req *v1.SearchArtistRequest) (*v1.SearchArtistResponse, error) {
@@ -206,13 +221,17 @@ func (s *Server) SearchArtists(ctx context.Context, req *v1.SearchArtistRequest)
limit := defaultLimit(int(req.Limit)) limit := defaultLimit(int(req.Limit))
offset := int(req.Offset) offset := int(req.Offset)
a, err := getArtists(s.Db, q, limit, offset) _, err := s.Sqlc.SearchArtists(ctx, sqlcdb.SearchArtistsParams{
Artist: sql.NullString{Valid: true, String: q},
Limit: int64(limit),
Offset: int64(offset),
})
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return nil, status.Error(codes.Internal, "failed to search artists") return nil, status.Error(codes.Internal, "failed to search artists")
} }
return &v1.SearchArtistResponse{ return &v1.SearchArtistResponse{
Artists: toProtoArtist(a), Artists: nil,
}, nil }, nil
} }

View File

@@ -10,54 +10,6 @@ import (
"database/sql" "database/sql"
) )
const getArtists = `-- name: GetArtists :many
SELECT Artist, COUNT(Artist) AS count
FROM Beatmap
WHERE Artist LIKE ? OR Title LIKE ?
GROUP BY Artist
LIMIT ? OFFSET ?
`
type GetArtistsParams struct {
Artist sql.NullString `json:"artist"`
Title sql.NullString `json:"title"`
Limit int64 `json:"limit"`
Offset int64 `json:"offset"`
}
type GetArtistsRow struct {
Artist sql.NullString `json:"artist"`
Count int64 `json:"count"`
}
func (q *Queries) GetArtists(ctx context.Context, arg GetArtistsParams) ([]GetArtistsRow, error) {
rows, err := q.db.QueryContext(ctx, getArtists,
arg.Artist,
arg.Title,
arg.Limit,
arg.Offset,
)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetArtistsRow{}
for rows.Next() {
var i GetArtistsRow
if err := rows.Scan(&i.Artist, &i.Count); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getBeatmapByHash = `-- name: GetBeatmapByHash :one const getBeatmapByHash = `-- name: GetBeatmapByHash :one
SELECT beatmapid, artist, artistunicode, title, titleunicode, creator, difficulty, audio, md5hash, file, rankedstatus, lastmodifiedtime, totaltime, audiopreviewtime, beatmapsetid, source, tags, lastplayed, folder FROM Beatmap WHERE MD5Hash = ? SELECT beatmapid, artist, artistunicode, title, titleunicode, creator, difficulty, audio, md5hash, file, rankedstatus, lastmodifiedtime, totaltime, audiopreviewtime, beatmapsetid, source, tags, lastplayed, folder FROM Beatmap WHERE MD5Hash = ?
` `
@@ -100,8 +52,19 @@ func (q *Queries) GetBeatmapCount(ctx context.Context) (int64, error) {
return count, err return count, err
} }
const getBeatmapSetCount = `-- name: GetBeatmapSetCount :one
SELECT COUNT(*) FROM Beatmap GROUP BY BeatmapSetId
`
func (q *Queries) GetBeatmapSetCount(ctx context.Context) (int64, error) {
row := q.db.QueryRowContext(ctx, getBeatmapSetCount)
var count int64
err := row.Scan(&count)
return count, err
}
const getRecentBeatmaps = `-- name: GetRecentBeatmaps :many const getRecentBeatmaps = `-- name: GetRecentBeatmaps :many
SELECT BeatmapId, MD5Hash, Title, Artist, Creator, Folder, File, Audio, TotalTime SELECT beatmapid, artist, artistunicode, title, titleunicode, creator, difficulty, audio, md5hash, file, rankedstatus, lastmodifiedtime, totaltime, audiopreviewtime, beatmapsetid, source, tags, lastplayed, folder
FROM Beatmap GROUP BY Folder ORDER BY LastModifiedTime DESC LIMIT ? OFFSET ? FROM Beatmap GROUP BY Folder ORDER BY LastModifiedTime DESC LIMIT ? OFFSET ?
` `
@@ -110,37 +73,35 @@ type GetRecentBeatmapsParams struct {
Offset int64 `json:"offset"` Offset int64 `json:"offset"`
} }
type GetRecentBeatmapsRow struct { func (q *Queries) GetRecentBeatmaps(ctx context.Context, arg GetRecentBeatmapsParams) ([]Beatmap, error) {
Beatmapid sql.NullInt64 `json:"beatmapid"`
Md5hash sql.NullString `json:"md5hash"`
Title sql.NullString `json:"title"`
Artist sql.NullString `json:"artist"`
Creator sql.NullString `json:"creator"`
Folder sql.NullString `json:"folder"`
File sql.NullString `json:"file"`
Audio sql.NullString `json:"audio"`
Totaltime sql.NullInt64 `json:"totaltime"`
}
func (q *Queries) GetRecentBeatmaps(ctx context.Context, arg GetRecentBeatmapsParams) ([]GetRecentBeatmapsRow, error) {
rows, err := q.db.QueryContext(ctx, getRecentBeatmaps, arg.Limit, arg.Offset) rows, err := q.db.QueryContext(ctx, getRecentBeatmaps, arg.Limit, arg.Offset)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
items := []GetRecentBeatmapsRow{} items := []Beatmap{}
for rows.Next() { for rows.Next() {
var i GetRecentBeatmapsRow var i Beatmap
if err := rows.Scan( if err := rows.Scan(
&i.Beatmapid, &i.Beatmapid,
&i.Md5hash,
&i.Title,
&i.Artist, &i.Artist,
&i.Artistunicode,
&i.Title,
&i.Titleunicode,
&i.Creator, &i.Creator,
&i.Folder, &i.Difficulty,
&i.File,
&i.Audio, &i.Audio,
&i.Md5hash,
&i.File,
&i.Rankedstatus,
&i.Lastmodifiedtime,
&i.Totaltime, &i.Totaltime,
&i.Audiopreviewtime,
&i.Beatmapsetid,
&i.Source,
&i.Tags,
&i.Lastplayed,
&i.Folder,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -176,13 +137,13 @@ type InsertBeatmapParams struct {
Md5hash sql.NullString `json:"md5hash"` Md5hash sql.NullString `json:"md5hash"`
File sql.NullString `json:"file"` File sql.NullString `json:"file"`
Rankedstatus sql.NullString `json:"rankedstatus"` Rankedstatus sql.NullString `json:"rankedstatus"`
Lastmodifiedtime sql.NullTime `json:"lastmodifiedtime"` Lastmodifiedtime sql.NullInt64 `json:"lastmodifiedtime"`
Totaltime sql.NullInt64 `json:"totaltime"` Totaltime sql.NullInt64 `json:"totaltime"`
Audiopreviewtime sql.NullInt64 `json:"audiopreviewtime"` Audiopreviewtime sql.NullInt64 `json:"audiopreviewtime"`
Beatmapsetid sql.NullInt64 `json:"beatmapsetid"` Beatmapsetid sql.NullInt64 `json:"beatmapsetid"`
Source sql.NullString `json:"source"` Source sql.NullString `json:"source"`
Tags sql.NullString `json:"tags"` Tags sql.NullString `json:"tags"`
Lastplayed sql.NullTime `json:"lastplayed"` Lastplayed sql.NullInt64 `json:"lastplayed"`
Folder sql.NullString `json:"folder"` Folder sql.NullString `json:"folder"`
} }
@@ -211,8 +172,56 @@ func (q *Queries) InsertBeatmap(ctx context.Context, arg InsertBeatmapParams) er
return err return err
} }
const searchArtists = `-- name: SearchArtists :many
SELECT Artist, COUNT(Artist) AS count
FROM Beatmap
WHERE Artist LIKE ? OR Title LIKE ?
GROUP BY Artist
LIMIT ? OFFSET ?
`
type SearchArtistsParams struct {
Artist sql.NullString `json:"artist"`
Title sql.NullString `json:"title"`
Limit int64 `json:"limit"`
Offset int64 `json:"offset"`
}
type SearchArtistsRow struct {
Artist sql.NullString `json:"artist"`
Count int64 `json:"count"`
}
func (q *Queries) SearchArtists(ctx context.Context, arg SearchArtistsParams) ([]SearchArtistsRow, error) {
rows, err := q.db.QueryContext(ctx, searchArtists,
arg.Artist,
arg.Title,
arg.Limit,
arg.Offset,
)
if err != nil {
return nil, err
}
defer rows.Close()
items := []SearchArtistsRow{}
for rows.Next() {
var i SearchArtistsRow
if err := rows.Scan(&i.Artist, &i.Count); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const searchBeatmaps = `-- name: SearchBeatmaps :many const searchBeatmaps = `-- name: SearchBeatmaps :many
SELECT BeatmapId, MD5Hash, Title, Artist, Creator, Folder, File, Audio, TotalTime SELECT beatmapid, artist, artistunicode, title, titleunicode, creator, difficulty, audio, md5hash, file, rankedstatus, lastmodifiedtime, totaltime, audiopreviewtime, beatmapsetid, source, tags, lastplayed, folder
FROM Beatmap FROM Beatmap
WHERE Title LIKE ? OR Artist LIKE ? WHERE Title LIKE ? OR Artist LIKE ?
LIMIT ? OFFSET ? LIMIT ? OFFSET ?
@@ -225,19 +234,7 @@ type SearchBeatmapsParams struct {
Offset int64 `json:"offset"` Offset int64 `json:"offset"`
} }
type SearchBeatmapsRow struct { func (q *Queries) SearchBeatmaps(ctx context.Context, arg SearchBeatmapsParams) ([]Beatmap, error) {
Beatmapid sql.NullInt64 `json:"beatmapid"`
Md5hash sql.NullString `json:"md5hash"`
Title sql.NullString `json:"title"`
Artist sql.NullString `json:"artist"`
Creator sql.NullString `json:"creator"`
Folder sql.NullString `json:"folder"`
File sql.NullString `json:"file"`
Audio sql.NullString `json:"audio"`
Totaltime sql.NullInt64 `json:"totaltime"`
}
func (q *Queries) SearchBeatmaps(ctx context.Context, arg SearchBeatmapsParams) ([]SearchBeatmapsRow, error) {
rows, err := q.db.QueryContext(ctx, searchBeatmaps, rows, err := q.db.QueryContext(ctx, searchBeatmaps,
arg.Title, arg.Title,
arg.Artist, arg.Artist,
@@ -248,19 +245,29 @@ func (q *Queries) SearchBeatmaps(ctx context.Context, arg SearchBeatmapsParams)
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
items := []SearchBeatmapsRow{} items := []Beatmap{}
for rows.Next() { for rows.Next() {
var i SearchBeatmapsRow var i Beatmap
if err := rows.Scan( if err := rows.Scan(
&i.Beatmapid, &i.Beatmapid,
&i.Md5hash,
&i.Title,
&i.Artist, &i.Artist,
&i.Artistunicode,
&i.Title,
&i.Titleunicode,
&i.Creator, &i.Creator,
&i.Folder, &i.Difficulty,
&i.File,
&i.Audio, &i.Audio,
&i.Md5hash,
&i.File,
&i.Rankedstatus,
&i.Lastmodifiedtime,
&i.Totaltime, &i.Totaltime,
&i.Audiopreviewtime,
&i.Beatmapsetid,
&i.Source,
&i.Tags,
&i.Lastplayed,
&i.Folder,
); err != nil { ); err != nil {
return nil, err return nil, err
} }

View File

@@ -10,82 +10,6 @@ import (
"database/sql" "database/sql"
) )
const getCollection = `-- name: GetCollection :many
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
JOIN (
SELECT DISTINCT Name
FROM Collection
ORDER BY Name
LIMIT 1 OFFSET ?
) selected_name ON c.Name = selected_name.Name
LIMIT ? OFFSET ?
`
type GetCollectionParams struct {
Offset int64 `json:"offset"`
Limit int64 `json:"limit"`
Offset_2 int64 `json:"offset_2"`
}
type GetCollectionRow struct {
Name sql.NullString `json:"name"`
Beatmapid sql.NullInt64 `json:"beatmapid"`
Md5hash sql.NullString `json:"md5hash"`
Title sql.NullString `json:"title"`
Artist sql.NullString `json:"artist"`
Creator sql.NullString `json:"creator"`
Folder sql.NullString `json:"folder"`
File sql.NullString `json:"file"`
Audio sql.NullString `json:"audio"`
Totaltime sql.NullInt64 `json:"totaltime"`
}
func (q *Queries) GetCollection(ctx context.Context, arg GetCollectionParams) ([]GetCollectionRow, error) {
rows, err := q.db.QueryContext(ctx, getCollection, arg.Offset, arg.Limit, arg.Offset_2)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetCollectionRow{}
for rows.Next() {
var i GetCollectionRow
if err := rows.Scan(
&i.Name,
&i.Beatmapid,
&i.Md5hash,
&i.Title,
&i.Artist,
&i.Creator,
&i.Folder,
&i.File,
&i.Audio,
&i.Totaltime,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getCollectionByName = `-- name: GetCollectionByName :many const getCollectionByName = `-- name: GetCollectionByName :many
SELECT c.Name, b.BeatmapId, b.MD5Hash, b.Title, b.Artist, b.Creator, b.Folder, b.File, b.Audio, b.TotalTime SELECT c.Name, b.BeatmapId, b.MD5Hash, b.Title, b.Artist, b.Creator, b.Folder, b.File, b.Audio, b.TotalTime
FROM Collection c FROM Collection c
@@ -147,6 +71,83 @@ func (q *Queries) GetCollectionByName(ctx context.Context, arg GetCollectionByNa
return items, nil return items, nil
} }
const getCollectionByOffset = `-- name: GetCollectionByOffset :many
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 Collection
GROUP BY Name
ORDER BY Name
LIMIT 1 OFFSET ?1
)
LIMIT ?2 OFFSET ?3
`
type GetCollectionByOffsetParams struct {
Offset int64 `json:"offset"`
Limit int64 `json:"limit"`
Offset_2 int64 `json:"offset_2"`
}
type GetCollectionByOffsetRow struct {
Name sql.NullString `json:"name"`
Beatmapid sql.NullInt64 `json:"beatmapid"`
Md5hash sql.NullString `json:"md5hash"`
Title sql.NullString `json:"title"`
Artist sql.NullString `json:"artist"`
Creator sql.NullString `json:"creator"`
Folder sql.NullString `json:"folder"`
File sql.NullString `json:"file"`
Audio sql.NullString `json:"audio"`
Totaltime sql.NullInt64 `json:"totaltime"`
}
func (q *Queries) GetCollectionByOffset(ctx context.Context, arg GetCollectionByOffsetParams) ([]GetCollectionByOffsetRow, error) {
rows, err := q.db.QueryContext(ctx, getCollectionByOffset, arg.Offset, arg.Limit, arg.Offset_2)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetCollectionByOffsetRow{}
for rows.Next() {
var i GetCollectionByOffsetRow
if err := rows.Scan(
&i.Name,
&i.Beatmapid,
&i.Md5hash,
&i.Title,
&i.Artist,
&i.Creator,
&i.Folder,
&i.File,
&i.Audio,
&i.Totaltime,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getCollectionCountByName = `-- name: GetCollectionCountByName :one const getCollectionCountByName = `-- name: GetCollectionCountByName :one
SELECT COUNT(*) FROM Collection WHERE Name = ? SELECT COUNT(*) FROM Collection WHERE Name = ?
` `
@@ -158,8 +159,26 @@ func (q *Queries) GetCollectionCountByName(ctx context.Context, name sql.NullStr
return count, err return count, err
} }
const getCollections = `-- name: GetCollections :many const insertCollection = `-- name: InsertCollection :exec
SELECT c.Name, COUNT(b.MD5Hash) AS Count, MIN(b.Folder) AS Folder, MIN(b.File) AS File INSERT INTO Collection (Name, MD5Hash) VALUES (?, ?)
`
type InsertCollectionParams struct {
Name sql.NullString `json:"name"`
Md5hash sql.NullString `json:"md5hash"`
}
func (q *Queries) InsertCollection(ctx context.Context, arg InsertCollectionParams) error {
_, err := q.db.ExecContext(ctx, insertCollection, arg.Name, arg.Md5hash)
return err
}
const searchCollection = `-- name: SearchCollection :many
SELECT
c.Name,
COUNT(b.MD5Hash) AS Count,
b.Folder,
b.File
FROM Collection c FROM Collection c
JOIN Beatmap b ON c.MD5Hash = b.MD5Hash JOIN Beatmap b ON c.MD5Hash = b.MD5Hash
WHERE c.Name LIKE ? WHERE c.Name LIKE ?
@@ -167,28 +186,28 @@ GROUP BY c.Name
LIMIT ? OFFSET ? LIMIT ? OFFSET ?
` `
type GetCollectionsParams struct { type SearchCollectionParams struct {
Name sql.NullString `json:"name"` Name sql.NullString `json:"name"`
Limit int64 `json:"limit"` Limit int64 `json:"limit"`
Offset int64 `json:"offset"` Offset int64 `json:"offset"`
} }
type GetCollectionsRow struct { type SearchCollectionRow struct {
Name sql.NullString `json:"name"` Name sql.NullString `json:"name"`
Count int64 `json:"count"` Count int64 `json:"count"`
Folder interface{} `json:"folder"` Folder sql.NullString `json:"folder"`
File interface{} `json:"file"` File sql.NullString `json:"file"`
} }
func (q *Queries) GetCollections(ctx context.Context, arg GetCollectionsParams) ([]GetCollectionsRow, error) { func (q *Queries) SearchCollection(ctx context.Context, arg SearchCollectionParams) ([]SearchCollectionRow, error) {
rows, err := q.db.QueryContext(ctx, getCollections, arg.Name, arg.Limit, arg.Offset) rows, err := q.db.QueryContext(ctx, searchCollection, arg.Name, arg.Limit, arg.Offset)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
items := []GetCollectionsRow{} items := []SearchCollectionRow{}
for rows.Next() { for rows.Next() {
var i GetCollectionsRow var i SearchCollectionRow
if err := rows.Scan( if err := rows.Scan(
&i.Name, &i.Name,
&i.Count, &i.Count,
@@ -207,17 +226,3 @@ func (q *Queries) GetCollections(ctx context.Context, arg GetCollectionsParams)
} }
return items, nil return items, nil
} }
const insertCollection = `-- name: InsertCollection :exec
INSERT INTO Collection (Name, MD5Hash) VALUES (?, ?)
`
type InsertCollectionParams struct {
Name sql.NullString `json:"name"`
Md5hash sql.NullString `json:"md5hash"`
}
func (q *Queries) InsertCollection(ctx context.Context, arg InsertCollectionParams) error {
_, err := q.db.ExecContext(ctx, insertCollection, arg.Name, arg.Md5hash)
return err
}

View File

@@ -20,13 +20,13 @@ type Beatmap struct {
Md5hash sql.NullString `json:"md5hash"` Md5hash sql.NullString `json:"md5hash"`
File sql.NullString `json:"file"` File sql.NullString `json:"file"`
Rankedstatus sql.NullString `json:"rankedstatus"` Rankedstatus sql.NullString `json:"rankedstatus"`
Lastmodifiedtime sql.NullTime `json:"lastmodifiedtime"` Lastmodifiedtime sql.NullInt64 `json:"lastmodifiedtime"`
Totaltime sql.NullInt64 `json:"totaltime"` Totaltime sql.NullInt64 `json:"totaltime"`
Audiopreviewtime sql.NullInt64 `json:"audiopreviewtime"` Audiopreviewtime sql.NullInt64 `json:"audiopreviewtime"`
Beatmapsetid sql.NullInt64 `json:"beatmapsetid"` Beatmapsetid sql.NullInt64 `json:"beatmapsetid"`
Source sql.NullString `json:"source"` Source sql.NullString `json:"source"`
Tags sql.NullString `json:"tags"` Tags sql.NullString `json:"tags"`
Lastplayed sql.NullTime `json:"lastplayed"` Lastplayed sql.NullInt64 `json:"lastplayed"`
Folder sql.NullString `json:"folder"` Folder sql.NullString `json:"folder"`
} }

View File

@@ -10,17 +10,18 @@ import (
) )
type Querier interface { type Querier interface {
GetArtists(ctx context.Context, arg GetArtistsParams) ([]GetArtistsRow, error)
GetBeatmapByHash(ctx context.Context, md5hash sql.NullString) (Beatmap, error) GetBeatmapByHash(ctx context.Context, md5hash sql.NullString) (Beatmap, error)
GetBeatmapCount(ctx context.Context) (int64, error) GetBeatmapCount(ctx context.Context) (int64, error)
GetCollection(ctx context.Context, arg GetCollectionParams) ([]GetCollectionRow, error) GetBeatmapSetCount(ctx context.Context) (int64, error)
GetCollectionByName(ctx context.Context, arg GetCollectionByNameParams) ([]GetCollectionByNameRow, error) GetCollectionByName(ctx context.Context, arg GetCollectionByNameParams) ([]GetCollectionByNameRow, error)
GetCollectionByOffset(ctx context.Context, arg GetCollectionByOffsetParams) ([]GetCollectionByOffsetRow, error)
GetCollectionCountByName(ctx context.Context, name sql.NullString) (int64, error) GetCollectionCountByName(ctx context.Context, name sql.NullString) (int64, error)
GetCollections(ctx context.Context, arg GetCollectionsParams) ([]GetCollectionsRow, error) GetRecentBeatmaps(ctx context.Context, arg GetRecentBeatmapsParams) ([]Beatmap, error)
GetRecentBeatmaps(ctx context.Context, arg GetRecentBeatmapsParams) ([]GetRecentBeatmapsRow, error)
InsertBeatmap(ctx context.Context, arg InsertBeatmapParams) error InsertBeatmap(ctx context.Context, arg InsertBeatmapParams) error
InsertCollection(ctx context.Context, arg InsertCollectionParams) error InsertCollection(ctx context.Context, arg InsertCollectionParams) error
SearchBeatmaps(ctx context.Context, arg SearchBeatmapsParams) ([]SearchBeatmapsRow, error) SearchArtists(ctx context.Context, arg SearchArtistsParams) ([]SearchArtistsRow, error)
SearchBeatmaps(ctx context.Context, arg SearchBeatmapsParams) ([]Beatmap, error)
SearchCollection(ctx context.Context, arg SearchCollectionParams) ([]SearchCollectionRow, error)
} }
var _ Querier = (*Queries)(nil) var _ Querier = (*Queries)(nil)

View File

@@ -1,125 +0,0 @@
package main
import (
v1 "backend/gen"
"backend/internal/db"
"fmt"
)
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"`
}
func (song Song) toProto() *v1.Song {
return &v1.Song{
BeatmapId: int32(song.BeatmapID),
Md5Hash: song.MD5Hash,
Title: song.Title,
Artist: song.Artist,
Creator: song.Creator,
Folder: song.Folder,
File: song.File,
Audio: song.Audio,
TotalTime: song.TotalTime,
Image: song.Image,
}
}
// The function `toSongProto` converts a `db.Beatmap` struct into a `v1.Song` struct by mapping the
// fields and performing some data type conversions.
func toSongProto(song db.Beatmap) *v1.Song {
fmt.Println(song)
return &v1.Song{
BeatmapId: int32(safeInt64(song.Beatmapid)),
Md5Hash: safeString(song.Md5hash),
Title: safeString(song.Title),
Artist: safeString(song.Artist),
Creator: safeString(song.Creator),
Folder: safeString(song.Folder),
File: safeString(song.File),
Audio: safeString(song.Audio),
TotalTime: int64(safeInt64(song.Totaltime)),
Image: extractImageFromFile(osuRoot, safeString(song.Folder), safeString(song.File)),
}
}
func toProtoSongs(local []Song) []*v1.Song {
songs := make([]*v1.Song, len(local))
for i, s := range local {
songs[i] = s.toProto()
}
return songs
}
// 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"`
}
func (c CollectionPreview) toProto() *v1.CollectionPreview {
return &v1.CollectionPreview{
Name: c.Name,
Image: c.Image,
Items: int32(c.Items),
}
}
func toProtoCollectionPreview(local []CollectionPreview) []*v1.CollectionPreview {
collection := make([]*v1.CollectionPreview, len(local))
for i, c := range local {
collection[i] = c.toProto()
}
return collection
}
// 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"`
}
func (a Artist) toProto() *v1.Artist {
return &v1.Artist{
Artist: a.Artist,
Items: int32(a.Count),
}
}
func toProtoArtist(local []Artist) []*v1.Artist {
artists := make([]*v1.Artist, len(local))
for i, a := range local {
artists[i] = a.toProto()
}
return artists
}

View File

@@ -12,3 +12,6 @@ sql:
emit_json_tags: true emit_json_tags: true
emit_interface: true emit_interface: true
emit_empty_slices: true emit_empty_slices: true
overrides:
- db_type: "INTEGER"
go_type: "int"

View File

@@ -12,17 +12,20 @@ SELECT * FROM Beatmap WHERE MD5Hash = ?;
-- name: GetBeatmapCount :one -- name: GetBeatmapCount :one
SELECT COUNT(*) FROM Beatmap; SELECT COUNT(*) FROM Beatmap;
-- name: GetBeatmapSetCount :one
SELECT COUNT(*) FROM Beatmap GROUP BY BeatmapSetId;
-- name: GetRecentBeatmaps :many -- name: GetRecentBeatmaps :many
SELECT BeatmapId, MD5Hash, Title, Artist, Creator, Folder, File, Audio, TotalTime SELECT *
FROM Beatmap GROUP BY Folder ORDER BY LastModifiedTime DESC LIMIT ? OFFSET ?; FROM Beatmap GROUP BY Folder ORDER BY LastModifiedTime DESC LIMIT ? OFFSET ?;
-- name: SearchBeatmaps :many -- name: SearchBeatmaps :many
SELECT BeatmapId, MD5Hash, Title, Artist, Creator, Folder, File, Audio, TotalTime SELECT *
FROM Beatmap FROM Beatmap
WHERE Title LIKE ? OR Artist LIKE ? WHERE Title LIKE ? OR Artist LIKE ?
LIMIT ? OFFSET ?; LIMIT ? OFFSET ?;
-- name: GetArtists :many -- name: SearchArtists :many
SELECT Artist, COUNT(Artist) AS count SELECT Artist, COUNT(Artist) AS count
FROM Beatmap FROM Beatmap
WHERE Artist LIKE ? OR Title LIKE ? WHERE Artist LIKE ? OR Title LIKE ?

View File

@@ -4,15 +4,19 @@ INSERT INTO Collection (Name, MD5Hash) VALUES (?, ?);
-- name: GetCollectionCountByName :one -- name: GetCollectionCountByName :one
SELECT COUNT(*) FROM Collection WHERE Name = ?; SELECT COUNT(*) FROM Collection WHERE Name = ?;
-- name: GetCollections :many -- name: SearchCollection :many
SELECT c.Name, COUNT(b.MD5Hash) AS Count, MIN(b.Folder) AS Folder, MIN(b.File) AS File SELECT
c.Name,
COUNT(b.MD5Hash) AS Count,
b.Folder,
b.File
FROM Collection c FROM Collection c
JOIN Beatmap b ON c.MD5Hash = b.MD5Hash JOIN Beatmap b ON c.MD5Hash = b.MD5Hash
WHERE c.Name LIKE ? WHERE c.Name LIKE ?
GROUP BY c.Name GROUP BY c.Name
LIMIT ? OFFSET ?; LIMIT ? OFFSET ?;
-- name: GetCollection :many -- name: GetCollectionByOffset :many
SELECT SELECT
c.Name, c.Name,
b.BeatmapId, b.BeatmapId,
@@ -26,13 +30,14 @@ SELECT
b.TotalTime b.TotalTime
FROM Collection c FROM Collection c
JOIN Beatmap b ON c.MD5Hash = b.MD5Hash JOIN Beatmap b ON c.MD5Hash = b.MD5Hash
JOIN ( WHERE c.Name = (
SELECT DISTINCT Name SELECT Name
FROM Collection FROM Collection
GROUP BY Name
ORDER BY Name ORDER BY Name
LIMIT 1 OFFSET ? LIMIT 1 OFFSET ?1
) selected_name ON c.Name = selected_name.Name )
LIMIT ? OFFSET ?; LIMIT ?2 OFFSET ?3;
-- name: GetCollectionByName :many -- name: GetCollectionByName :many
SELECT c.Name, b.BeatmapId, b.MD5Hash, b.Title, b.Artist, b.Creator, b.Folder, b.File, b.Audio, b.TotalTime SELECT c.Name, b.BeatmapId, b.MD5Hash, b.Title, b.Artist, b.Creator, b.Folder, b.File, b.Audio, b.TotalTime

View File

@@ -11,13 +11,13 @@ CREATE TABLE IF NOT EXISTS Beatmap (
MD5Hash TEXT DEFAULT '00000000000000000000000000000000', MD5Hash TEXT DEFAULT '00000000000000000000000000000000',
File TEXT DEFAULT 'unknown.osu', File TEXT DEFAULT 'unknown.osu',
RankedStatus TEXT DEFAULT 'Unknown', RankedStatus TEXT DEFAULT 'Unknown',
LastModifiedTime DATETIME DEFAULT '0001-01-01 00:00:00', LastModifiedTime INTEGER DEFAULT 0,
TotalTime INTEGER DEFAULT 0, TotalTime INTEGER DEFAULT 0,
AudioPreviewTime INTEGER DEFAULT 0, AudioPreviewTime INTEGER DEFAULT 0,
BeatmapSetId INTEGER DEFAULT -1, BeatmapSetId INTEGER DEFAULT -1,
Source TEXT DEFAULT '', Source TEXT DEFAULT '',
Tags TEXT DEFAULT '', Tags TEXT DEFAULT '',
LastPlayed DATETIME DEFAULT '0001-01-01 00:00:00', LastPlayed INTEGER DEFAULT 0,
Folder TEXT DEFAULT 'Unknown Folder', Folder TEXT DEFAULT 'Unknown Folder',
UNIQUE (Artist, Title, MD5Hash, Difficulty) UNIQUE (Artist, Title, MD5Hash, Difficulty)
); );

View File

@@ -0,0 +1,96 @@
package main
import (
v1 "backend/gen"
"backend/internal/db"
)
func toProtoSongsSqlC(songs []db.Beatmap) []*v1.Song {
protoSongs := make([]*v1.Song, len(songs))
for i, s := range songs {
protoSongs[i] = toProtoSongSqlC(s)
}
return protoSongs
}
func toProtoSongSqlC(song db.Beatmap) *v1.Song {
return &v1.Song{
BeatmapId: int32(song.Beatmapid.Int64),
Md5Hash: song.Md5hash.String,
Title: song.Title.String,
Artist: song.Artist.String,
Creator: song.Creator.String,
Folder: song.Folder.String,
File: song.File.String,
Audio: song.Audio.String,
TotalTime: song.Totaltime.Int64,
Image: extractImageFromFile(osuRoot, song.Folder.String, song.File.String),
}
}
func toProtoCollectionSqlc(rows []db.GetCollectionByNameRow) *v1.CollectionResponse {
songs := make([]*v1.Song, len(rows))
for i, b := range rows {
songs[i] = toProtoSongSqlC(db.Beatmap{
Beatmapid: b.Beatmapid,
Md5hash: b.Md5hash,
Title: b.Title,
Artist: b.Artist,
Creator: b.Creator,
Folder: b.Folder,
File: b.File,
Audio: b.Audio,
Totaltime: b.Totaltime,
})
}
return &v1.CollectionResponse{
Name: rows[0].Name.String,
Items: int32(len(rows)),
Songs: songs,
}
}
func toProtoCollectionoffsetSqlc(rows []db.GetCollectionByOffsetRow) *v1.CollectionResponse {
songs := make([]*v1.Song, len(rows))
for i, b := range rows {
songs[i] = toProtoSongSqlC(db.Beatmap{
Beatmapid: b.Beatmapid,
Md5hash: b.Md5hash,
Title: b.Title,
Artist: b.Artist,
Creator: b.Creator,
Folder: b.Folder,
File: b.File,
Audio: b.Audio,
Totaltime: b.Totaltime,
})
}
return &v1.CollectionResponse{
Name: rows[0].Name.String,
Items: int32(len(rows)),
Songs: songs,
}
}
func toProtoCollectionsSearchPreview(rows []db.SearchCollectionRow) *v1.SearchCollectionResponse {
collection := make([]*v1.CollectionPreview, len(rows))
for i, c := range rows {
collection[i] = toProtoCollectionPreviewSqlc(c)
}
return &v1.SearchCollectionResponse{Collections: collection}
}
func toProtoCollectionPreviewSqlc(row db.SearchCollectionRow) *v1.CollectionPreview {
return &v1.CollectionPreview{
Name: row.Name.String,
Items: int32(row.Count),
Image: extractImageFromFile(osuRoot, row.Folder.String, row.File.String),
}
}