ripple-api/beatmapget/beatmapget.go

186 lines
4.5 KiB
Go

// Package beatmapget is an helper package to retrieve beatmap information from
// the osu! API, if the beatmap in the database is too old.
package beatmapget
import (
"database/sql"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/osuyozora/api/common"
"github.com/jmoiron/sqlx"
"gopkg.in/thehowl/go-osuapi.v1"
)
// Expire is the duration after which a beatmap expires.
const Expire = time.Hour * 12
// DB is the database.
var DB *sqlx.DB
// Client is the osu! api client to use
var Client *osuapi.Client
// BeatmapDefiningQuality is the defining quality of the beatmap to be updated,
// which is to say either the ID, the set ID or the md5 hash.
type BeatmapDefiningQuality struct {
ID int
MD5 string
frozen bool
ranked int
}
func (b BeatmapDefiningQuality) String() string {
if b.MD5 != "" {
return "#/" + b.MD5
}
if b.ID != 0 {
return "b/" + strconv.Itoa(b.ID)
}
return "n/a"
}
func (b BeatmapDefiningQuality) isSomethingSet() error {
if b.ID == 0 && b.MD5 == "" {
return errors.New("beatmapget: at least one field in BeatmapDefiningQuality must be set")
}
return nil
}
func (b BeatmapDefiningQuality) whereAndParams() (where string, params []interface{}) {
var wheres []string
if b.ID != 0 {
wheres = append(wheres, "beatmap_id = ?")
params = append(params, b.ID)
}
if b.MD5 != "" {
wheres = append(wheres, "beatmap_md5 = ?")
params = append(params, b.MD5)
}
where = strings.Join(wheres, " AND ")
if where == "" {
where = "1"
}
return
}
// UpdateIfRequired updates the beatmap in the database if an update is required.
func UpdateIfRequired(b BeatmapDefiningQuality) error {
required, err := UpdateRequired(&b)
if err != nil && err != sql.ErrNoRows {
return err
}
if !required {
return nil
}
return Update(b, err != sql.ErrNoRows)
}
// UpdateRequired checks an update is required. If error is sql.ErrNoRows,
// it means that the beatmap should be updated, and that there was not the
// beatmap in the database previously.
func UpdateRequired(b *BeatmapDefiningQuality) (bool, error) {
if err := b.isSomethingSet(); err != nil {
return false, err
}
where, params := b.whereAndParams()
var data struct {
Difficulties [3]float64
Ranked int
Frozen bool
LatestUpdate common.UnixTimestamp
}
err := DB.QueryRow("SELECT difficulty_taiko, difficulty_ctb, difficulty_mania, ranked,"+
"ranked_status_freezed, latest_update FROM beatmaps WHERE "+
where+" LIMIT 1", params...).
Scan(
&data.Difficulties[0], &data.Difficulties[1], &data.Difficulties[2],
&data.Ranked, &data.Frozen, &data.LatestUpdate,
)
b.frozen = data.Frozen
if b.frozen {
b.ranked = data.Ranked
}
if err != nil {
if err == sql.ErrNoRows {
return true, err
}
return false, err
}
if data.Difficulties[0] == 0 && data.Difficulties[1] == 0 && data.Difficulties[2] == 0 {
return true, nil
}
expire := Expire
if data.Ranked == 2 {
expire *= 6
}
if expire != 0 && time.Now().After(time.Time(data.LatestUpdate).Add(expire)) && !data.Frozen {
return true, nil
}
return false, nil
}
// Update updates a beatmap.
func Update(b BeatmapDefiningQuality, beatmapInDB bool) error {
var data [4]osuapi.Beatmap
for i := 0; i <= 3; i++ {
mode := osuapi.Mode(i)
beatmaps, err := Client.GetBeatmaps(osuapi.GetBeatmapsOpts{
BeatmapID: b.ID,
BeatmapHash: b.MD5,
Mode: &mode,
})
if err != nil {
return err
}
if len(beatmaps) == 0 {
continue
}
data[i] = beatmaps[0]
}
var main *osuapi.Beatmap
for _, el := range data {
if el.FileMD5 != "" {
main = &el
break
}
}
if main == nil {
return fmt.Errorf("beatmapget: beatmap %s not found", b.String())
}
if beatmapInDB {
w, p := b.whereAndParams()
DB.MustExec("DELETE FROM beatmaps WHERE "+w, p...)
}
if b.frozen {
main.Approved = osuapi.ApprovedStatus(b.ranked)
}
songName := fmt.Sprintf("%s - %s [%s]", main.Artist, main.Title, main.DiffName)
_, err := DB.Exec(`INSERT INTO
beatmaps (
beatmap_id, beatmapset_id, beatmap_md5,
song_name, ar, od, difficulty_std, difficulty_taiko,
difficulty_ctb, difficulty_mania, max_combo, hit_length,
bpm, ranked, latest_update, ranked_status_freezed
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`,
main.BeatmapID, main.BeatmapSetID, main.FileMD5,
songName, main.ApproachRate, main.OverallDifficulty, data[0].DifficultyRating, data[1].DifficultyRating,
data[2].DifficultyRating, data[3].DifficultyRating, main.MaxCombo, main.HitLength,
int(main.BPM), main.Approved, time.Now().Unix(), b.frozen,
)
if err != nil {
return err
}
return nil
}
func init() {
osuapi.RateLimit(200)
}