From b81dffcecce0cbe2d85a010506ea25c6c3691037 Mon Sep 17 00:00:00 2001 From: Howl Date: Wed, 7 Sep 2016 01:51:23 +0200 Subject: [PATCH] add bgeatmapget --- beatmapget/beatmapget.go | 186 +++++++++++++++++++++++++++++++++++++++ beatmapget/fullset.go | 63 +++++++++++++ common/conf.go | 1 + main.go | 6 ++ 4 files changed, 256 insertions(+) create mode 100644 beatmapget/beatmapget.go create mode 100644 beatmapget/fullset.go diff --git a/beatmapget/beatmapget.go b/beatmapget/beatmapget.go new file mode 100644 index 0000000..69b64d6 --- /dev/null +++ b/beatmapget/beatmapget.go @@ -0,0 +1,186 @@ +// 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" + + "git.zxq.co/ripple/rippleapi/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) +} diff --git a/beatmapget/fullset.go b/beatmapget/fullset.go new file mode 100644 index 0000000..2f89bf7 --- /dev/null +++ b/beatmapget/fullset.go @@ -0,0 +1,63 @@ +package beatmapget + +import ( + "time" + + "git.zxq.co/ripple/rippleapi/common" + "gopkg.in/thehowl/go-osuapi.v1" +) + +// Set checks if an update is required for all beatmaps in a set. +func Set(s int) error { + var ( + lastUpdated common.UnixTimestamp + ranked int + ) + err := DB.QueryRow("SELECT last_updated, ranked FROM beatmaps WHERE beatmapset_id = ? LIMIT 1", s). + Scan(&lastUpdated, &ranked) + if err != nil { + return err + } + return set(s, lastUpdated, ranked) +} + +// Beatmap check if an update is required for all beatmaps in the set +// containing this beatmap. +func Beatmap(b int) error { + var ( + setID int + lastUpdated common.UnixTimestamp + ranked int + ) + err := DB.QueryRow("SELECT beatmapset_id, last_updated, ranked FROM beatmaps WHERE beatmap_id = ? LIMIT 1", b). + Scan(&setID, &lastUpdated, &ranked) + if err != nil { + return err + } + return set(setID, lastUpdated, ranked) +} + +func set(s int, updated common.UnixTimestamp, ranked int) error { + expire := Expire + if ranked == 2 { + expire *= 6 + } + if time.Now().Before(time.Time(updated).Add(expire)) { + return nil + } + beatmaps, err := Client.GetBeatmaps(osuapi.GetBeatmapsOpts{ + BeatmapSetID: s, + }) + if err != nil { + return err + } + for _, beatmap := range beatmaps { + err := UpdateIfRequired(BeatmapDefiningQuality{ + ID: beatmap.BeatmapID, + }) + if err != nil { + return err + } + } + return nil +} diff --git a/common/conf.go b/common/conf.go index 73971a4..d9d83d7 100644 --- a/common/conf.go +++ b/common/conf.go @@ -21,6 +21,7 @@ type Conf struct { HanayoKey string BeatmapRequestsPerUser int RankQueueSize int + OsuAPIKey string } var cachedConf *Conf diff --git a/main.go b/main.go index d665908..90e015f 100644 --- a/main.go +++ b/main.go @@ -7,11 +7,13 @@ import ( "syscall" "git.zxq.co/ripple/rippleapi/app" + "git.zxq.co/ripple/rippleapi/beatmapget" "git.zxq.co/ripple/rippleapi/common" "git.zxq.co/ripple/schiavolib" // Golint pls dont break balls _ "github.com/go-sql-driver/mysql" "github.com/jmoiron/sqlx" + "gopkg.in/thehowl/go-osuapi.v1" ) // Version is the git hash of the application. Do not edit. This is @@ -54,6 +56,10 @@ func main() { schiavo.Bunker.Send(err.Error()) log.Fatalln(err) } + + beatmapget.Client = osuapi.NewClient(conf.OsuAPIKey) + beatmapget.DB = db + engine := app.Start(conf, db) startuato(engine)