From 33d98731d2ba23ec1f786fc3e8cb26ca71977b10 Mon Sep 17 00:00:00 2001 From: lixiangwuxian Date: Sun, 20 Oct 2024 01:26:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0beatleader=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- handler/beatleader/beatleader.go | 8 + handler/scoresaber/score.go | 6 + service/beatleader/bind_bl.go | 304 +++++++++++++++ service/beatleader/gen_picture.go | 23 ++ service/beatleader/get_blid.go | 27 ++ service/beatleader/hot.go | 78 ++++ service/beatleader/model.go | 617 ++++++++++++++++++++++++++++++ service/beatleader/user_info.go | 66 ++++ service/scoresaber/bind_ss.go | 4 +- service/scoresaber/model.go | 22 +- 10 files changed, 1151 insertions(+), 4 deletions(-) create mode 100644 service/beatleader/bind_bl.go create mode 100644 service/beatleader/gen_picture.go create mode 100644 service/beatleader/get_blid.go create mode 100644 service/beatleader/hot.go create mode 100644 service/beatleader/model.go create mode 100644 service/beatleader/user_info.go diff --git a/handler/beatleader/beatleader.go b/handler/beatleader/beatleader.go index 2e4aca8..e2d573d 100644 --- a/handler/beatleader/beatleader.go +++ b/handler/beatleader/beatleader.go @@ -11,11 +11,13 @@ import ( func init() { handler.RegisterHandler("查bl", getMyBL) + handler.RegisterHelpInform("查bl", "查看您的最新分数") handler.RegisterHandler("绑定bl", bindBL) handler.RegisterHandler("解绑bl", unbindBL) handler.RegisterHandler("最新bl", getMyRecentScore) handler.RegisterHandler("最热bl", getRecentScore) handler.RegisterHandler("截图bl", screenShotBL) + handler.RegisterHandler("jbl", screenShotBL) } func getMyBL(msg model.Message) (reply model.Reply) { @@ -82,6 +84,9 @@ func getRecentScore(msg model.Message) (reply model.Reply) { FromMsg: msg, } } + if count > 10 { + count = 10 + } } scoreMsg := "" for _, v := range beatleader.BlScoresManager.GetRecentScores(count, " WHERE country = 'CN' ") { @@ -110,6 +115,9 @@ func getMyRecentScore(msg model.Message) (reply model.Reply) { FromMsg: msg, } } + if count > 10 { + count = 10 + } } var userName string recordCount := 0 diff --git a/handler/scoresaber/score.go b/handler/scoresaber/score.go index 7beefc2..f3da3f3 100644 --- a/handler/scoresaber/score.go +++ b/handler/scoresaber/score.go @@ -107,6 +107,9 @@ func getRecentScore(msg model.Message) (reply model.Reply) { FromMsg: msg, } } + if count > 10 { + count = 10 + } } scoreMsg := "" for _, v := range scoresaber.ScoresManager.GetRecentScores(count, " WHERE country = 'CN' ") { @@ -135,6 +138,9 @@ func getMyRecentScore(msg model.Message) (reply model.Reply) { FromMsg: msg, } } + if count > 10 { + count = 10 + } } var userName string recordCount := 0 diff --git a/service/beatleader/bind_bl.go b/service/beatleader/bind_bl.go new file mode 100644 index 0000000..b9ccdb1 --- /dev/null +++ b/service/beatleader/bind_bl.go @@ -0,0 +1,304 @@ +package beatleader + +import ( + "database/sql" + "errors" + "log" + "strconv" + "time" + + "git.lxtend.com/qqbot/sqlite3" + _ "github.com/mattn/go-sqlite3" +) + +func init() { + createScoreTableSQL := `CREATE TABLE IF NOT EXISTS blData ( + id TEXT, + name TEXT, + country TEXT, + pp REAL, + rank INTEGER, + country_rank INTEGER, + total_score INTEGER, + total_ranked_score INTEGER, + average_ranked_accuracy REAL, + total_play_count INTEGER, + ranked_play_count INTEGER, + replays_watched INTEGER, + generated_time TEXT + );` + + createRecordTableSQL := `CREATE TABLE IF NOT EXISTS blRecordData ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + score_id INT, + bl_id TEXT, + name TEXT, + country TEXT, + song_name VARCHAR(255), + song_sub_name VARCHAR(255), + song_author_name VARCHAR(255), + song_hash VARCHAR(64), + cover_image TEXT, + difficulty_raw VARCHAR(100), + pp REAL, + stars REAL, + weight REAL, + modifiers VARCHAR(255), + multiplier REAL, + bad_cuts INT, + missed_notes INT, + max_combo INT, + score INT, + rank INT, + max_score INT, + full_combo BOOLEAN, + device_hmd VARCHAR(100), + device_controller_left VARCHAR(100), + device_controller_right VARCHAR(100), + generated_time TEXT + );` + + sqlite3.TryCreateTable(createScoreTableSQL) + sqlite3.TryCreateTable(createRecordTableSQL) +} + +var BLQuery = &blQuery{} + +type blQuery struct { +} + +func (bl *blQuery) BindBL(qqId string, blId string) (reply string) { + tx, err := sqlite3.GetTran() + if err != nil { + log.Print(err) + } + defer tx.Rollback() + // blId为数字 + if _, isNum := strconv.Atoi(blId); isNum != nil { + return "blId格式错误,应当为一串数字" + } + data, _ := FetchPlayerData(blId) + if data == nil { + return "未找到玩家" + } + //去重 + if rows, err := tx.Query("SELECT * FROM ssBind WHERE qqid = ?", qqId); err == nil { + if rows.Next() { + return "您已绑定过bl账号,请先输入\"解绑bl\"解绑" + } + rows.Close() + } + _, err = tx.Exec("INSERT INTO ssBind(qqid, ssid) VALUES(?, ?)", qqId, blId) + if err != nil { + return "绑定失败" + } + err = tx.Commit() + if err != nil { + return "无法提交事务" + } + return "和用户名为 " + data.Name + " 的用户绑定成功,输入\"查bl\"查看个人数据" +} + +func (bl *blQuery) UnbindBL(qqId string) (reply string) { + tx, err := sqlite3.GetTran() + if err != nil { + log.Print(err) + } + defer tx.Rollback() + //是否已绑定 + if rows, err := tx.Query("SELECT * FROM ssBind WHERE qqid = ?", qqId); err == nil { + if !rows.Next() { + return "您未绑定bl账号,输入\"绑定bl [blId]\"绑定" + } + rows.Close() + } + _, err = tx.Exec("DELETE FROM ssBind WHERE qqid = ?", qqId) + if err != nil { + return "解绑失败" + } + err = tx.Commit() + if err != nil { + return "无法提交事务" + } + return "解绑成功,重新绑定请输入\"绑定bl [blId]\"" +} + +func (bl *blQuery) GetScore(qqId string) (reply string, err error) { + db := sqlite3.GetDB() // 假设 sqlite3.GetDB() 返回 *sqlx.DB + tx, err := db.Beginx() + if err != nil { + log.Print(err) + return "数据库连接失败,请稍后重试", err + } + defer tx.Rollback() + + blId, err := getBLID(qqId) + if err != nil { + return err.Error(), nil + } + + // 查询玩家数据 + data, _ := FetchPlayerData(blId) + if data == nil { + return "查询出错,服务器返回了空数据", errors.New("查询出错,服务器返回了空数据") + } + + // 构建 PlayerDataLite 结构体 + dataLite := PlayerDataLite{ + ID: data.ID, + Name: data.Name, + Country: data.Country, + PP: data.AccPp + data.PassPp + data.TechPp, + Rank: data.Rank, + CountryRank: data.CountryRank, + TotalScore: data.ScoreStats.TotalScore, + TotalRankedScore: data.ScoreStats.TotalRankedScore, + AverageRankedAccuracy: data.ScoreStats.AverageRankedAccuracy, + TotalPlayCount: data.ScoreStats.TotalPlayCount, + RankedPlayCount: data.ScoreStats.RankedPlayCount, + ReplaysWatched: data.ScoreStats.WatchedReplays, + GeneratedTime: time.Now().Format("2006-01-02 15:04:05.999999999-07:00"), + } + + // 查询最近的玩家数据 + var lastDataLite PlayerDataLite + err = tx.Get(&lastDataLite, "SELECT * FROM blData WHERE id = ? ORDER BY generated_time DESC LIMIT 1", dataLite.ID) + if err != nil && err != sql.ErrNoRows { + log.Print(err) + return "查询历史数据时出错", err + } + + // 如果有新的数据,则插入 + if lastDataLite.TotalPlayCount != dataLite.TotalPlayCount { + _, err = tx.NamedExec(`INSERT INTO blData (id, name, country, pp, rank, country_rank, total_score, + total_ranked_score, average_ranked_accuracy, total_play_count, ranked_play_count, replays_watched, generated_time) + VALUES (:id, :name, :country, :pp, :rank, :country_rank, :total_score, + :total_ranked_score, :average_ranked_accuracy, :total_play_count, :ranked_play_count, :replays_watched, :generated_time)`, dataLite) + if err != nil { + log.Print(err) + return "插入新数据时出错", err + } + + // 提交事务 + err = tx.Commit() + if err != nil { + log.Print(err) + return "SQL事务提交失败,请重试", err + } + + // 返回差异信息 + return data.LastDiffToString(lastDataLite), nil + } + + // 如果没有新数据,直接提交事务 + err = tx.Commit() + if err != nil { + log.Print(err) + return "SQL事务提交失败,请重试", err + } + + // 返回当前数据的字符串表示 + return data.ToString(), nil +} + +func (bl *blQuery) SaveRecord(scoreData ScoreData) { + db := sqlite3.GetDB() // 假设 sqlite3.GetDB() 返回 *sqlx.DB + tx, err := db.Beginx() + if err != nil { + log.Print(err) + return + } + defer tx.Rollback() + + // 创建 RecordDataLite 结构体实例 + dataLite := RecordDataLite{ + ScoreID: scoreData.ID, + BlID: scoreData.Player.ID, + Name: scoreData.Player.Name, + Country: scoreData.Player.Country, + SongName: scoreData.Leaderboard.Song.Name, + SongSubName: scoreData.Leaderboard.Song.SubName, + SongAuthorName: scoreData.Leaderboard.Song.Author, + SongHash: scoreData.Leaderboard.Song.Hash, + CoverImage: scoreData.Leaderboard.Song.CoverImage, + DifficultyRaw: scoreData.Leaderboard.Difficulty.DifficultyName, + Stars: 0, + PP: scoreData.Pp, + Weight: scoreData.Weight, + Modifiers: scoreData.Modifiers, + Multiplier: float64(scoreData.ModifiedScore) / float64(scoreData.BaseScore), + BadCuts: scoreData.BadCuts, + Score: scoreData.ModifiedScore, + Rank: scoreData.Rank, + MaxScore: int(float64(scoreData.ModifiedScore) / scoreData.Accuracy), + MissedNotes: scoreData.MissedNotes, + MaxCombo: scoreData.MaxCombo, + FullCombo: scoreData.FullCombo, + DeviceHmd: GetHMDStr(scoreData.HMD), + DeviceControllerLeft: GetControllerStr(scoreData.Controller), + DeviceControllerRight: GetControllerStr(scoreData.Controller), + GeneratedTime: time.Now().Format("2006-01-02 15:04:05.999999999-07:00"), + } + if scoreData.Leaderboard.Difficulty.Stars != nil { + dataLite.Stars = *scoreData.Leaderboard.Difficulty.Stars + } + + // 使用 NamedExec 插入数据 + _, err = tx.NamedExec(`INSERT INTO blRecordData + (score_id, bl_id, name, country, song_name, song_sub_name, song_author_name, + song_hash, cover_image, difficulty_raw, pp, stars, weight, modifiers, + multiplier, bad_cuts, missed_notes, max_combo, score,rank, max_score, + full_combo, device_hmd, device_controller_left, device_controller_right, + generated_time) + VALUES (:score_id, :bl_id, :name, :country, :song_name, :song_sub_name, + :song_author_name, :song_hash, :cover_image, :difficulty_raw, :pp, :stars, + :weight, :modifiers, :multiplier, :bad_cuts, :missed_notes, :max_combo, + :score,:rank, :max_score, :full_combo, :device_hmd, :device_controller_left, + :device_controller_right, :generated_time)`, dataLite) + if err != nil { + log.Print(err) + return + } + + // 提交事务 + err = tx.Commit() + if err != nil { + log.Print(err) + } +} + +func (bl *blQuery) GetRecentScores(count int, qqId string) ([]RecordDataLite, error) { + db := sqlite3.GetDB() // 假设 sqlite3.GetDB() 返回 *sqlx.DB + tx, err := db.Beginx() + if err != nil { + log.Print(err) + return nil, errors.New("数据库连接失败,请稍后重试") + } + defer tx.Rollback() + + // 查询绑定的 blId + blId, err := getBLID(qqId) + if err != nil { + return nil, err + } + + // 查询记录 + var records []RecordDataLite + err = tx.Select(&records, "SELECT * FROM blRecordData WHERE bl_id = ? ORDER BY generated_time DESC LIMIT ?", blId, count) + if err != nil { + if err == sql.ErrNoRows { + return nil, errors.New("未查询到数据") + } + log.Println("查询数据出错:", err) + return nil, errors.New("查询记录失败") + } + + // 提交事务 + err = tx.Commit() + if err != nil { + log.Print(err) + return nil, errors.New("提交事务失败") + } + + return records, nil +} diff --git a/service/beatleader/gen_picture.go b/service/beatleader/gen_picture.go new file mode 100644 index 0000000..ea47410 --- /dev/null +++ b/service/beatleader/gen_picture.go @@ -0,0 +1,23 @@ +package beatleader + +import ( + "fmt" + + "git.lxtend.com/qqbot/util" +) + +func GetBLPicture(qqId string) (outputImgName string) { + blId, err := getBLID(qqId) + if err != nil { + return "" + } + url := fmt.Sprintf("https://beatleader.xyz/u/%s", blId) + outputImgPath := fmt.Sprintf("./tmp/beatleader_%s.png", blId) + outputImgName = fmt.Sprintf("beatleader_%s.png", blId) + if err := util.ScreenshotURL(url, outputImgPath, 1420, 2280, 70, 50, 160, 140, "chartjs"); err != nil { + fmt.Println(err) + return "" + } + + return outputImgName +} diff --git a/service/beatleader/get_blid.go b/service/beatleader/get_blid.go new file mode 100644 index 0000000..22f8c68 --- /dev/null +++ b/service/beatleader/get_blid.go @@ -0,0 +1,27 @@ +package beatleader + +import ( + "database/sql" + "errors" + "log" + + "git.lxtend.com/qqbot/sqlite3" +) + +func getBLID(qqId string) (blId string, err error) { + + db := sqlite3.GetDB() // 假设 sqlite3.GetDB() 返回 *sqlx.DB + if err != nil { + log.Print(err) + return "", errors.New("数据库连接失败,请稍后重试") + } + err = db.Get(&blId, "SELECT ssid FROM ssBind WHERE qqid = ?", qqId) + if err != nil { + if err == sql.ErrNoRows { + return "", errors.New("未绑定bl账号,输入\"绑定bl [blId]\"绑定") + } + log.Println("查询 blId 出错:", err) + return "", errors.New("查询 blId 失败") + } + return blId, nil +} diff --git a/service/beatleader/hot.go b/service/beatleader/hot.go new file mode 100644 index 0000000..c80f19e --- /dev/null +++ b/service/beatleader/hot.go @@ -0,0 +1,78 @@ +package beatleader + +import ( + "log" + "time" + + "git.lxtend.com/qqbot/sqlite3" + "github.com/gorilla/websocket" +) + +const wsURL = "wss://sockets.api.beatleader.xyz/scores" + +var BlScoresManager = blScoresManager{} + +type blScoresManager struct { + conn *websocket.Conn + retryTimes int +} + +func init() { + for err := BlScoresManager.connect(); err != nil; err = BlScoresManager.connect() { + log.Print("连接 WebSocket 失败:", err) + time.Sleep(time.Second) + } +} + +func (bm *blScoresManager) connect() error { + var err error + bm.conn, _, err = websocket.DefaultDialer.Dial(wsURL, nil) + if err != nil { + return err + } + bm.retryTimes = 0 + go bm.receiveData() + return nil +} + +func (sm *blScoresManager) receiveData() { + defer func() { + for err := BlScoresManager.connect(); err != nil; err = BlScoresManager.connect() { + log.Printf("连接 WebSocket 失败:%v", err) + time.Sleep(time.Second) + } + }() + for { + var scoreData ScoreData + err := sm.conn.ReadJSON(&scoreData) + if err != nil { + log.Print("读取数据失败:", err) + time.Sleep(time.Second) + sm.retryTimes++ + if sm.retryTimes > 3 { + return + } + continue + } + BLQuery.SaveRecord(scoreData) + } +} + +func (sm *blScoresManager) GetRecentScores(count int, predict string) []RecordDataLite { + db := sqlite3.GetDB() + scoresCopy := make([]RecordDataLite, 0, count) + + query := "SELECT * FROM blRecordData" + if predict != "" { + query += " " + predict + } + query += " ORDER BY generated_time DESC LIMIT ?" + + err := db.Select(&scoresCopy, query, count) + if err != nil { + log.Print(err) + return nil + } + + return scoresCopy +} diff --git a/service/beatleader/model.go b/service/beatleader/model.go new file mode 100644 index 0000000..8b1a622 --- /dev/null +++ b/service/beatleader/model.go @@ -0,0 +1,617 @@ +package beatleader + +import ( + "fmt" + "time" +) + +type ScoreData struct { + ContextExtensions []ContextExtension `json:"contextExtensions"` + MyScore *string `json:"myScore"` + ValidContexts int `json:"validContexts"` + Leaderboard Leaderboard `json:"leaderboard"` + AccLeft float64 `json:"accLeft"` + AccRight float64 `json:"accRight"` + ID int `json:"id"` + BaseScore int `json:"baseScore"` + ModifiedScore int `json:"modifiedScore"` + Accuracy float64 `json:"accuracy"` + PlayerID string `json:"playerId"` + Pp float64 `json:"pp"` + BonusPp float64 `json:"bonusPp"` + PassPp float64 `json:"passPP"` + AccPp float64 `json:"accPP"` + TechPp float64 `json:"techPP"` + Rank int `json:"rank"` + Country string `json:"country"` + FcAccuracy float64 `json:"fcAccuracy"` + FcPp float64 `json:"fcPp"` + Weight float64 `json:"weight"` + Replay string `json:"replay"` + Modifiers string `json:"modifiers"` + BadCuts int `json:"badCuts"` + MissedNotes int `json:"missedNotes"` + BombCuts int `json:"bombCuts"` + WallsHit int `json:"wallsHit"` + Pauses int `json:"pauses"` + FullCombo bool `json:"fullCombo"` + Platform string `json:"platform"` + MaxCombo int `json:"maxCombo"` + MaxStreak *int `json:"maxStreak"` + HMD int `json:"hmd"` + Controller int `json:"controller"` + LeaderboardID string `json:"leaderboardId"` + Timeset string `json:"timeset"` + Timepost int64 `json:"timepost"` + ReplaysWatched int `json:"replaysWatched"` + PlayCount int `json:"playCount"` + LastTryTime int64 `json:"lastTryTime"` + Priority int `json:"priority"` + Player Player `json:"player"` + ScoreImprovement ScoreImprovement `json:"scoreImprovement"` + // RankVoting *string `json:"rankVoting"` + // Metadata *string `json:"metadata"` + Offsets Offsets `json:"offsets"` +} + +type ContextExtension struct { + ID int `json:"id"` + PlayerID string `json:"playerId"` + Weight float64 `json:"weight"` + Rank int `json:"rank"` + BaseScore int `json:"baseScore"` + ModifiedScore int `json:"modifiedScore"` + Accuracy float64 `json:"accuracy"` + Pp float64 `json:"pp"` + PassPp float64 `json:"passPP"` + AccPp float64 `json:"accPP"` + TechPp float64 `json:"techPP"` + BonusPp float64 `json:"bonusPp"` + Modifiers string `json:"modifiers"` + Context int `json:"context"` + ScoreImprovement ScoreImprovement `json:"scoreImprovement"` +} + +type ScoreImprovement struct { + ID int `json:"id"` + Timeset string `json:"timeset"` + Score int `json:"score"` + Accuracy float64 `json:"accuracy"` + Pp float64 `json:"pp"` + BonusPp float64 `json:"bonusPp"` + Rank int `json:"rank"` + AccRight float64 `json:"accRight"` + AccLeft float64 `json:"accLeft"` + AverageRankedAccuracy float64 `json:"averageRankedAccuracy"` + TotalPp float64 `json:"totalPp"` + TotalRank int `json:"totalRank"` + BadCuts int `json:"badCuts"` + MissedNotes int `json:"missedNotes"` + BombCuts int `json:"bombCuts"` + WallsHit int `json:"wallsHit"` + Pauses int `json:"pauses"` +} + +type Leaderboard struct { + ID string `json:"id"` + Song Song `json:"song"` + Difficulty Difficulty `json:"difficulty"` +} + +type Song struct { + ID string `json:"id"` + Hash string `json:"hash"` + Name string `json:"name"` + SubName string `json:"subName"` + Author string `json:"author"` + Mapper string `json:"mapper"` + MapperID int `json:"mapperId"` + CollaboratorIds *string `json:"collaboratorIds"` + CoverImage string `json:"coverImage"` + Bpm float64 `json:"bpm"` + Duration float64 `json:"duration"` + FullCoverImage *string `json:"fullCoverImage"` +} + +type Difficulty struct { + ID int `json:"id"` + Value int `json:"value"` + Mode int `json:"mode"` + DifficultyName string `json:"difficultyName"` + ModeName string `json:"modeName"` + Status int `json:"status"` + ModifierValues ModifierValues `json:"modifierValues"` + // ModifiersRating *string `json:"modifiersRating"` + NominatedTime int64 `json:"nominatedTime"` + QualifiedTime int64 `json:"qualifiedTime"` + RankedTime int64 `json:"rankedTime"` + SpeedTags int `json:"speedTags"` + StyleTags int `json:"styleTags"` + FeatureTags int `json:"featureTags"` + Stars *float64 `json:"stars"` + PredictedAcc float64 `json:"predictedAcc"` +} + +type ModifierValues struct { + ModifierID int `json:"modifierId"` + Da float64 `json:"da"` + Fs float64 `json:"fs"` + Sf float64 `json:"sf"` + // 其他修改器字段... +} + +type Player struct { + ID string `json:"id"` + Name string `json:"name"` + Platform string `json:"platform"` + Avatar string `json:"avatar"` + Country string `json:"country"` + Alias *string `json:"alias"` + Bot bool `json:"bot"` + Pp float64 `json:"pp"` + Rank int `json:"rank"` + CountryRank int `json:"countryRank"` + Role string `json:"role"` + Socials *string `json:"socials"` + ContextExtensions *string `json:"contextExtensions"` +} + +type Offsets struct { + ID int `json:"id"` + Frames int `json:"frames"` + Notes int `json:"notes"` + Walls int `json:"walls"` + Heights int `json:"heights"` + Pauses int `json:"pauses"` +} + +// 表示记录的数据,本地储存 +type RecordDataLite struct { + ID int `json:"id" db:"id"` + ScoreID int `json:"scoreId" db:"score_id"` + BlID string `json:"blId" db:"bl_id"` + Name string `json:"name" db:"name"` + Country string `json:"country" db:"country"` + SongName string `json:"songName" db:"song_name"` + SongSubName string `json:"songSubName" db:"song_sub_name"` + SongAuthorName string `json:"songAuthorName" db:"song_author_name"` + SongHash string `json:"songHash" db:"song_hash"` + CoverImage string `json:"coverImage" db:"cover_image"` + DifficultyRaw string `json:"difficultyRaw" db:"difficulty_raw"` + Stars float64 `json:"stars" db:"stars"` + PP float64 `json:"pp" db:"pp"` + Weight float64 `json:"weight" db:"weight"` + Modifiers string `json:"modifiers" db:"modifiers"` + Multiplier float64 `json:"multiplier" db:"multiplier"` + Rank int `json:"rank" db:"rank"` + BadCuts int `json:"badCuts" db:"bad_cuts"` + MissedNotes int `json:"missedNotes" db:"missed_notes"` + MaxCombo int `json:"maxCombo" db:"max_combo"` + Score int `json:"score" db:"score"` + MaxScore int `json:"maxScore" db:"max_score"` + FullCombo bool `json:"fullCombo" db:"full_combo"` + DeviceHmd string `json:"deviceHmd" db:"device_hmd"` + DeviceControllerLeft string `json:"deviceControllerLeft" db:"device_controller_left"` + DeviceControllerRight string `json:"deviceControllerRight" db:"device_controller_right"` + GeneratedTime string `json:"generatedTime" db:"generated_time"` +} + +func (r RecordDataLite) ToString() string { + formatedStrRanked := "%s,%s 使用 %s 在 %s 的 %s 难度(%.1f🌟)中打到了全球排名第%d,pp 为 %.2f,准度为 %s。" + formatedStrUnranked := "%s, %s 使用 %s 在 %s 的 %s 难度中打到了全球排名第%d,准度为 %s。" + formatedStrWithoutDevice := "%s, %s 在 %s 的 %s 难度(%.1f🌟)中打到了全球排名第%d,pp 为 %.2f,准度为 %s。" + formatedStrWithoutDeviceAndRank := "%s, %s 在 %s 的 %s 难度中打到了全球排名第%d,准度为 %s。" + hardStr := r.DifficultyRaw + layout := "2006-01-02 15:04:05.999999999-07:00" + parsedTime, _ := time.Parse(layout, r.GeneratedTime) + duration := time.Since(parsedTime) + timeStr := timeConvert(duration) + if r.Stars == 0 && r.DeviceHmd != "" { + return fmt.Sprintf(formatedStrUnranked, timeStr, r.Name, r.DeviceHmd, r.SongName, hardStr, r.Rank, fmt.Sprintf("%.2f%%", float64(r.Score)/float64(r.MaxScore)*100)) + } else if r.Stars != 0 && r.DeviceHmd != "" { + return fmt.Sprintf(formatedStrRanked, timeStr, r.Name, r.DeviceHmd, r.SongName, hardStr, r.Stars, r.Rank, r.PP, fmt.Sprintf("%.2f%%", float64(r.Score)/float64(r.MaxScore)*100)) + } else if r.Stars != 0 && r.DeviceHmd == "" { + return fmt.Sprintf(formatedStrWithoutDevice, timeStr, r.Name, r.SongName, hardStr, r.Stars, r.Rank, r.PP, fmt.Sprintf("%.2f%%", float64(r.Score)/float64(r.MaxScore)*100)) + } else { + return fmt.Sprintf(formatedStrWithoutDeviceAndRank, timeStr, r.Name, r.SongName, hardStr, r.Rank, fmt.Sprintf("%.2f%%", float64(r.Score)/float64(r.MaxScore)*100)) + } +} + +func timeConvert(duration time.Duration) string { + var result string + if duration.Hours() >= 24 { + days := int(duration.Hours() / 24) + result = fmt.Sprintf("%d天前", days) + } else if duration.Hours() >= 1 { + hours := int(duration.Hours()) + result = fmt.Sprintf("%d小时前", hours) + } else if duration.Minutes() >= 1 { + minutes := int(duration.Minutes()) + result = fmt.Sprintf("%d分钟前", minutes) + } else { + result = "刚刚" + } + return result +} + +//用户信息 + +// ScoreStats 存储分数统计信息 +type PlayerData struct { + MapperID int `json:"mapperId"` + Banned bool `json:"banned"` + Inactive bool `json:"inactive"` + BanDescription *string `json:"banDescription"` + ExternalProfileURL string `json:"externalProfileUrl"` + RichBioTimeset int64 `json:"richBioTimeset"` + SpeedrunStart int64 `json:"speedrunStart"` + LinkedIDs LinkedIDs `json:"linkedIds"` + History *string `json:"history"` + Badges []string `json:"badges"` + PinnedScores *string `json:"pinnedScores"` + Changes []Change `json:"changes"` + AccPp float64 `json:"accPp"` + PassPp float64 `json:"passPp"` + TechPp float64 `json:"techPp"` + ScoreStats ScoreStats `json:"scoreStats"` + LastWeekPp float64 `json:"lastWeekPp"` + LastWeekRank int `json:"lastWeekRank"` + LastWeekCountryRank int `json:"lastWeekCountryRank"` + ExtensionID int `json:"extensionId"` + ID string `json:"id"` + Name string `json:"name"` + Platform string `json:"platform"` + Avatar string `json:"avatar"` + Country string `json:"country"` + Alias *string `json:"alias"` + Bot bool `json:"bot"` + Pp float64 `json:"pp"` + Rank int `json:"rank"` + CountryRank int `json:"countryRank"` + Role string `json:"role"` + Socials []string `json:"socials"` + ContextExtensions *string `json:"contextExtensions"` + PatreonFeatures *string `json:"patreonFeatures"` + ProfileSettings ProfileSettings `json:"profileSettings"` + ClanOrder string `json:"clanOrder"` + Clans []string `json:"clans"` +} + +type LinkedIDs struct { + QuestID int `json:"questId"` + SteamID string `json:"steamId"` + OculusPCID string `json:"oculusPCId"` +} + +type Change struct { + ID int64 `json:"id"` + Timestamp int64 `json:"timestamp"` + PlayerID string `json:"playerId"` + OldName string `json:"oldName"` + NewName string `json:"newName"` + OldCountry string `json:"oldCountry"` + NewCountry string `json:"newCountry"` + Changer *string `json:"changer"` +} + +type ScoreStats struct { + ID int64 `json:"id"` + TotalScore int64 `json:"totalScore"` + TotalUnrankedScore int64 `json:"totalUnrankedScore"` + TotalRankedScore int64 `json:"totalRankedScore"` + LastScoreTime int64 `json:"lastScoreTime"` + LastUnrankedScoreTime int64 `json:"lastUnrankedScoreTime"` + LastRankedScoreTime int64 `json:"lastRankedScoreTime"` + AverageRankedAccuracy float64 `json:"averageRankedAccuracy"` + AverageWeightedRankedAccuracy float64 `json:"averageWeightedRankedAccuracy"` + AverageUnrankedAccuracy float64 `json:"averageUnrankedAccuracy"` + AverageAccuracy float64 `json:"averageAccuracy"` + MedianRankedAccuracy float64 `json:"medianRankedAccuracy"` + MedianAccuracy float64 `json:"medianAccuracy"` + TopRankedAccuracy float64 `json:"topRankedAccuracy"` + TopUnrankedAccuracy float64 `json:"topUnrankedAccuracy"` + TopAccuracy float64 `json:"topAccuracy"` + TopPp float64 `json:"topPp"` + TopBonusPP float64 `json:"topBonusPP"` + TopPassPP float64 `json:"topPassPP"` + TopAccPP float64 `json:"topAccPP"` + TopTechPP float64 `json:"topTechPP"` + PeakRank int `json:"peakRank"` + RankedMaxStreak int `json:"rankedMaxStreak"` + UnrankedMaxStreak int `json:"unrankedMaxStreak"` + MaxStreak int `json:"maxStreak"` + AverageLeftTiming float64 `json:"averageLeftTiming"` + AverageRightTiming float64 `json:"averageRightTiming"` + RankedPlayCount int `json:"rankedPlayCount"` + UnrankedPlayCount int `json:"unrankedPlayCount"` + TotalPlayCount int `json:"totalPlayCount"` + RankedImprovementsCount int `json:"rankedImprovementsCount"` + UnrankedImprovementsCount int `json:"unrankedImprovementsCount"` + TotalImprovementsCount int `json:"totalImprovementsCount"` + RankedTop1Count int `json:"rankedTop1Count"` + UnrankedTop1Count int `json:"unrankedTop1Count"` + Top1Count int `json:"top1Count"` + RankedTop1Score int64 `json:"rankedTop1Score"` + UnrankedTop1Score int64 `json:"unrankedTop1Score"` + Top1Score int64 `json:"top1Score"` + AverageRankedRank float64 `json:"averageRankedRank"` + AverageWeightedRankedRank float64 `json:"averageWeightedRankedRank"` + AverageUnrankedRank float64 `json:"averageUnrankedRank"` + AverageRank float64 `json:"averageRank"` + SspPlays int `json:"sspPlays"` + SsPlays int `json:"ssPlays"` + SpPlays int `json:"spPlays"` + SPlays int `json:"sPlays"` + APlays int `json:"aPlays"` + TopPlatform string `json:"topPlatform"` + TopHMD int `json:"topHMD"` + AllHMDs string `json:"allHMDs"` + TopPercentile float64 `json:"topPercentile"` + CountryTopPercentile float64 `json:"countryTopPercentile"` + DailyImprovements int `json:"dailyImprovements"` + AuthorizedReplayWatched int `json:"authorizedReplayWatched"` + AnonimusReplayWatched int `json:"anonimusReplayWatched"` + WatchedReplays int `json:"watchedReplays"` +} + +type ProfileSettings struct { + ID int64 `json:"id"` + Bio *string `json:"bio"` + Message *string `json:"message"` + EffectName string `json:"effectName"` + ProfileAppearance string `json:"profileAppearance"` + Hue int `json:"hue"` + Saturation float64 `json:"saturation"` + LeftSaberColor *string `json:"leftSaberColor"` + RightSaberColor *string `json:"rightSaberColor"` + ProfileCover *string `json:"profileCover"` + StarredFriends string `json:"starredFriends"` + HorizontalRichBio bool `json:"horizontalRichBio"` + RankedMapperSort *string `json:"rankedMapperSort"` + ShowBots bool `json:"showBots"` + ShowAllRatings bool `json:"showAllRatings"` + ShowStatsPublic bool `json:"showStatsPublic"` + ShowStatsPublicPinned bool `json:"showStatsPublicPinned"` +} + +type PlayerDataLite struct { + ID string `json:"id" db:"id"` + Name string `json:"name" db:"name"` + Country string `json:"country" db:"country"` + Device string `json:"device" db:"device"` + PP float64 `json:"pp" db:"pp"` + Rank int `json:"rank" db:"rank"` + CountryRank int `json:"countryRank" db:"country_rank"` + TotalScore int64 `json:"totalScore" db:"total_score"` + TotalRankedScore int64 `json:"totalRankedScore" db:"total_ranked_score"` + AverageRankedAccuracy float64 `json:"averageRankedAccuracy" db:"average_ranked_accuracy"` + TotalPlayCount int `json:"totalPlayCount" db:"total_play_count"` + RankedPlayCount int `json:"rankedPlayCount" db:"ranked_play_count"` + ReplaysWatched int `json:"replaysWatched" db:"replays_watched"` + GeneratedTime string `json:"generatedTime" db:"generated_time"` +} + +func (p PlayerData) ToString() string { + formatedStr := "玩家 %s\n" + + "区域 %s\n" + + "PP %.1f\n" + + "全球排名 %d\n" + + "区域排名 %d\n" + + "Ranked谱面均准 %.2f%%\n" + + "总游玩次数 %d\n" + + "Ranked谱面游玩次数 %d\n" + + "回放被观看次数 %d" + return fmt.Sprintf(formatedStr, + p.Name, + p.Country, + p.Pp, + p.Rank, + p.CountryRank, + p.ScoreStats.AverageRankedAccuracy*100, + p.ScoreStats.TotalPlayCount, + p.ScoreStats.RankedPlayCount, + p.ScoreStats.WatchedReplays) +} + +func (p PlayerDataLite) ToString() string { + formatedStr := "玩家 %s\n" + + "区域 %s\n" + + "PP %.1f\n" + + "全球排名 %d\n" + + "区域排名 %d\n" + + "Ranked谱面均准 %.2f%%\n" + + "总游玩次数 %d\n" + + "Ranked谱面游玩次数 %d\n" + + "回放被观看次数 %d" + return fmt.Sprintf(formatedStr, + p.Name, + p.Country, + p.PP, + p.Rank, + p.CountryRank, + p.AverageRankedAccuracy*100, + p.TotalPlayCount, + p.RankedPlayCount, + p.ReplaysWatched) +} + +func (p PlayerData) LastDiffToString(lastDayQueryData PlayerDataLite) string { + formatedStr := "玩家 %s\n" + + "区域 %s\n" + + "PP %.1f(%+.1f)\n" + + "全球排名 %d(%+d)\n" + + "区域排名 %d(%+d)\n" + + "Ranked谱面均准 %.2f%%(%+.2f%%)\n" + + "总游玩次数 %d(%+d)\n" + + "Ranked谱面游玩次数 %d(%+d)\n" + + "回放被观看次数 %d" + return fmt.Sprintf(formatedStr, + p.Name, + p.Country, + p.Pp, p.Pp-lastDayQueryData.PP, + p.Rank, lastDayQueryData.Rank-p.Rank, + p.CountryRank, lastDayQueryData.CountryRank-p.CountryRank, + p.ScoreStats.AverageRankedAccuracy*100, (p.ScoreStats.AverageRankedAccuracy-lastDayQueryData.AverageRankedAccuracy)*100, + p.ScoreStats.TotalPlayCount, p.ScoreStats.TotalPlayCount-lastDayQueryData.TotalPlayCount, + p.ScoreStats.RankedPlayCount, p.ScoreStats.RankedPlayCount-lastDayQueryData.RankedPlayCount, + p.ScoreStats.WatchedReplays) +} + +func GetControllerStr(controller int) string { + switch controller { + case 1: + return "Oculus Touch 手柄" + case 16: + return "Oculus Touch 2 手柄" + case 79: + return "Meta Quest 3 手柄" + case 256: + return "Quest 2 手柄" + case 2: + return "VIVE wands" + case 4: + return "VIVE 2 wands" + case 128: + return "VIVE cosmos controllers" + case 8: + return "WMR 手柄" + case 33: + return "Pico 手柄" + case 34: + return "Pico 手柄" + case 35: + return "VIVE Pro dudads" + case 37: + return "Miramar 手柄" + case 44: + return "disco 手柄" + case 61: + return "Quest Pro 手柄" + case 62: + return "VIVE tracker" + case 63: + return "VIVE tracker 2" + case 64: + return "Knuckles" + case 65: + return "nolo手柄" + case 66: + return "Pico phoenix" + case 67: + return "双手🙌" + case 68: + return "VIVE tracker 3" + case 75: + return "游戏手柄🎮" + case 76: + return "Joy-Con" + case 77: + return "Steam Deck" + case 78: + return "Etee" + } + return "未知手柄" +} + +func GetHMDStr(hmd int) string { + switch hmd { + case 256: + return "Quest 2" + case 512: + return "Quest 3" + case 64: + return "Valve Index" + case 513: + return "Quest 3S" + case 1: + return "Rift CV1" + case 2: + return "Vive" + case 60: + return "Pico 4" + case 61: + return "Quest Pro" + case 70: + return "PS VR2" + case 8: + return "WMR" + case 16: + return "Rift S" + case 65: + return "Controllable" + case 32: + return "Quest" + case 4: + return "Vive Pro" + case 35: + return "Vive Pro 2" + case 128: + return "Vive Cosmos" + case 36: + return "Vive Elite" + case 47: + return "Vive Focus" + case 38: + return "Pimax 8K" + case 39: + return "Pimax 5K" + case 40: + return "Pimax Artisan" + case 33: + return "Pico Neo 3" + case 34: + return "Pico Neo 2" + case 41: + return "HP Reverb" + case 42: + return "Samsung WMR" + case 43: + return "Qiyu Dream" + case 45: + return "Lenovo Explorer" + case 46: + return "Acer WMR" + case 66: + return "Bigscreen Beyond" + case 67: + return "NOLO Sonic" + case 68: + return "Hypereal" + case 48: + return "Arpara" + case 49: + return "Dell Visor" + case 71: + return "MeganeX VG1" + case 55: + return "Huawei VR" + case 56: + return "Asus WMR" + case 51: + return "Vive DVT" + case 52: + return "glasses20" + case 53: + return "Varjo" + case 69: + return "Varjo Aero" + case 54: + return "Vaporeon" + case 57: + return "Cloud XR" + case 58: + return "VRidge" + case 50: + return "e3" + case 59: + return "Medion Eraser" + case 37: + return "Miramar" + case 0: + return "Unknown headset" + case 44: + return "Disco" + default: + return "神秘头显" + } +} diff --git a/service/beatleader/user_info.go b/service/beatleader/user_info.go new file mode 100644 index 0000000..aca96f4 --- /dev/null +++ b/service/beatleader/user_info.go @@ -0,0 +1,66 @@ +package beatleader + +import ( + "compress/gzip" + "encoding/json" + "fmt" + "io" + "net/http" +) + +// fetchPlayerData 函数请求 BeatLeader API,并解析完整的玩家信息 +func FetchPlayerData(blID string) (*PlayerData, error) { + url := fmt.Sprintf("https://api.beatleader.xyz/player/%s?leaderboardContext=general", blID) + + // 创建请求 + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + // 设置请求头 + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0") + req.Header.Set("Accept", "application/json, text/plain, */*") + req.Header.Set("Accept-Language", "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2") + req.Header.Set("Accept-Encoding", "gzip") + req.Header.Set("DNT", "1") + req.Header.Set("Connection", "keep-alive") + req.Header.Set("Referer", fmt.Sprintf("https://beatleader.xyz/u/%s/scores/date/desc/1", blID)) + req.Header.Set("Sec-Fetch-Dest", "empty") + req.Header.Set("Sec-Fetch-Mode", "cors") + req.Header.Set("Sec-Fetch-Site", "same-origin") + req.Header.Set("Sec-GPC", "1") + req.Header.Set("Pragma", "no-cache") + req.Header.Set("Cache-Control", "no-cache") + req.Header.Set("TE", "trailers") + + // 发送请求 + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // 处理压缩响应 + var reader io.Reader + if resp.Header.Get("Content-Encoding") == "gzip" { + reader, err = gzip.NewReader(resp.Body) + if err != nil { + return nil, err + } + defer reader.(*gzip.Reader).Close() + } else { + reader = resp.Body + } + + // 解析响应体 + var playerData PlayerData + err = json.NewDecoder(reader).Decode(&playerData) + if err != nil { + // log.Printf("got body %v", reader.) + return nil, err + } + + return &playerData, nil +} diff --git a/service/scoresaber/bind_ss.go b/service/scoresaber/bind_ss.go index b4f4d34..528a40f 100644 --- a/service/scoresaber/bind_ss.go +++ b/service/scoresaber/bind_ss.go @@ -267,12 +267,12 @@ func (ss *ssQuery) SaveRecord(cmdData CommandData) { _, err = tx.NamedExec(`INSERT INTO ssRecordData (score_id, ss_id, name, country, song_name, song_sub_name, song_author_name, song_hash, cover_image, difficulty_raw, pp, stars, weight, modifiers, - multiplier, bad_cuts, missed_notes, max_combo, score, max_score, + multiplier,rank, bad_cuts, missed_notes, max_combo, score, max_score, full_combo, device_hmd, device_controller_left, device_controller_right, generated_time) VALUES (:score_id, :ss_id, :name, :country, :song_name, :song_sub_name, :song_author_name, :song_hash, :cover_image, :difficulty_raw, :pp, :stars, - :weight, :modifiers, :multiplier, :bad_cuts, :missed_notes, :max_combo, + :weight, :modifiers, :multiplier, :rank,:bad_cuts, :missed_notes, :max_combo, :score, :max_score, :full_combo, :device_hmd, :device_controller_left, :device_controller_right, :generated_time)`, dataLite) if err != nil { diff --git a/service/scoresaber/model.go b/service/scoresaber/model.go index c28d0db..ebddd20 100644 --- a/service/scoresaber/model.go +++ b/service/scoresaber/model.go @@ -238,7 +238,16 @@ func (p PlayerData) ToString() string { "总游玩次数 %d\n" + "Ranked谱面游玩次数 %d\n" + "回放被观看次数 %d" - return fmt.Sprintf(formatedStr, p.Name, p.Country, p.PP, p.Rank, p.CountryRank, p.ScoreStats.AverageRankedAccuracy, p.ScoreStats.TotalPlayCount, p.ScoreStats.RankedPlayCount, p.ScoreStats.ReplaysWatched) + return fmt.Sprintf(formatedStr, + p.Name, + p.Country, + p.PP, + p.Rank, + p.CountryRank, + p.ScoreStats.AverageRankedAccuracy, + p.ScoreStats.TotalPlayCount, + p.ScoreStats.RankedPlayCount, + p.ScoreStats.ReplaysWatched) } func (p PlayerDataLite) ToString() string { @@ -251,7 +260,16 @@ func (p PlayerDataLite) ToString() string { "总游玩次数 %d\n" + "Ranked谱面游玩次数 %d\n" + "回放被观看次数 %d" - return fmt.Sprintf(formatedStr, p.Name, p.Country, p.PP, p.Rank, p.CountryRank, p.AverageRankedAccuracy, p.TotalPlayCount, p.RankedPlayCount, p.ReplaysWatched) + return fmt.Sprintf(formatedStr, + p.Name, + p.Country, + p.PP, + p.Rank, + p.CountryRank, + p.AverageRankedAccuracy, + p.TotalPlayCount, + p.RankedPlayCount, + p.ReplaysWatched) } func (p PlayerData) LastDiffToString(lastDayQueryData PlayerDataLite) string {