2016-09-06 23:51:23 +00:00
|
|
|
// 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"
|
|
|
|
|
2019-02-23 13:09:10 +00:00
|
|
|
"github.com/osuYozora/rippleapi/common"
|
2016-09-06 23:51:23 +00:00
|
|
|
"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
|
|
|
|
}
|
|
|
|
|
2016-09-20 16:14:02 +00:00
|
|
|
if expire != 0 && time.Now().After(time.Time(data.LatestUpdate).Add(expire)) && !data.Frozen {
|
2016-09-06 23:51:23 +00:00
|
|
|
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)
|
|
|
|
}
|