159 lines
3.8 KiB
Go
159 lines
3.8 KiB
Go
package models
|
|
|
|
import (
|
|
"bytes"
|
|
"database/sql"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// SearchOptions are options that can be passed to SearchSets for filtering
|
|
// sets.
|
|
type SearchOptions struct {
|
|
// If len is 0, then it should be treated as if all statuses are good.
|
|
Status []int
|
|
Query string
|
|
// Gamemodes to which limit the results. If len is 0, it means all modes
|
|
// are ok.
|
|
Mode []int
|
|
|
|
// Pagination options.
|
|
Offset int
|
|
Amount int
|
|
}
|
|
|
|
func (o SearchOptions) setModes() (total uint8) {
|
|
for _, m := range o.Mode {
|
|
if m < 0 || m >= 4 {
|
|
continue
|
|
}
|
|
total |= 1 << uint8(m)
|
|
}
|
|
return
|
|
}
|
|
|
|
var mysqlStringReplacer = strings.NewReplacer(
|
|
`\`, `\\`,
|
|
`"`, `\"`,
|
|
`'`, `\'`,
|
|
"\x00", `\0`,
|
|
"\n", `\n`,
|
|
"\r", `\r`,
|
|
"\x1a", `\Z`,
|
|
)
|
|
|
|
func sIntCommaSeparated(nums []int) string {
|
|
b := bytes.Buffer{}
|
|
for idx, num := range nums {
|
|
b.WriteString(strconv.Itoa(num))
|
|
if idx != len(nums)-1 {
|
|
b.WriteString(", ")
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// SearchSets retrieves sets, filtering them using SearchOptions.
|
|
func SearchSets(db, searchDB *sql.DB, opts SearchOptions) ([]Set, error) {
|
|
sm := strconv.Itoa(int(opts.setModes()))
|
|
setIDsQuery := "SELECT id, set_modes & " + sm + " AS valid_set_modes FROM cg WHERE "
|
|
|
|
// add filters to query
|
|
// Yes. I know. Prepared statements. But Sphinx doesn't like them, so
|
|
// bummer.
|
|
setIDsQuery += "MATCH('" + mysqlStringReplacer.Replace(opts.Query) + "') "
|
|
if len(opts.Status) != 0 {
|
|
setIDsQuery += "AND ranked_status IN (" + sIntCommaSeparated(opts.Status) + ") "
|
|
}
|
|
if len(opts.Mode) != 0 {
|
|
// This is a hack. Apparently, Sphinx does not support AND bitwise
|
|
// operations in the WHERE clause, so we're placing that in the SELECT
|
|
// clause and only making sure it's correct in this place.
|
|
setIDsQuery += "AND valid_set_modes = " + sm + " "
|
|
}
|
|
|
|
// set limit
|
|
setIDsQuery += fmt.Sprintf("ORDER BY WEIGHT() DESC, id DESC LIMIT %d, %d OPTION ranker=sph04", opts.Offset, opts.Amount)
|
|
|
|
// fetch rows
|
|
rows, err := searchDB.Query(setIDsQuery)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// from the rows we will retrieve the IDs of all our sets.
|
|
// we also pre-create the slices containing the sets we will fill later on
|
|
// when we fetch the actual data.
|
|
setIDs := make([]int, 0, opts.Amount)
|
|
sets := make([]Set, 0, opts.Amount)
|
|
// setMap, having an ID, points to a position of a set contained in sets.
|
|
setMap := make(map[int]int, opts.Amount)
|
|
for rows.Next() {
|
|
var id int
|
|
err = rows.Scan(&id, new(int))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
setIDs = append(setIDs, id)
|
|
sets = append(sets, Set{})
|
|
setMap[id] = len(sets) - 1
|
|
}
|
|
|
|
// short circuit: there are no sets
|
|
if len(sets) == 0 {
|
|
return []Set{}, nil
|
|
}
|
|
|
|
setsQuery := "SELECT " + setFields + " FROM sets WHERE id IN (" + inClause(len(setIDs)) + ")"
|
|
args := sIntToSInterface(setIDs)
|
|
|
|
rows, err = db.Query(setsQuery, args...)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// find all beatmaps, but leave children aside for the moment.
|
|
for rows.Next() {
|
|
var s Set
|
|
err = rows.Scan(
|
|
&s.ID, &s.RankedStatus, &s.ApprovedDate, &s.LastUpdate, &s.LastChecked,
|
|
&s.Artist, &s.Title, &s.Creator, &s.Source, &s.Tags, &s.HasVideo, &s.Genre,
|
|
&s.Language, &s.Favourites,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sets[setMap[s.ID]] = s
|
|
}
|
|
|
|
rows, err = db.Query(
|
|
"SELECT "+beatmapFields+" FROM beatmaps WHERE parent_set_id IN ("+
|
|
inClause(len(setIDs))+")",
|
|
sIntToSInterface(setIDs)...,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for rows.Next() {
|
|
var b Beatmap
|
|
err = rows.Scan(
|
|
&b.ID, &b.ParentSetID, &b.DiffName, &b.FileMD5, &b.Mode, &b.BPM,
|
|
&b.AR, &b.OD, &b.CS, &b.HP, &b.TotalLength, &b.HitLength,
|
|
&b.Playcount, &b.Passcount, &b.MaxCombo, &b.DifficultyRating,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parentSet, ok := setMap[b.ParentSetID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
sets[parentSet].ChildrenBeatmaps = append(sets[parentSet].ChildrenBeatmaps, b)
|
|
}
|
|
|
|
return sets, nil
|
|
}
|