diff options
| author | NeKz <NeKzor@users.noreply.github.com> | 2024-11-16 08:24:03 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-11-16 10:24:03 +0300 |
| commit | 5fb47b69d895fcbe98fc714b47057b0051387e05 (patch) | |
| tree | 0de4a1bd6a55d08c04f6153dc08a7e178aa71e52 | |
| parent | fix/frontend: broken youtube url (#231) (diff) | |
| download | lphub-5fb47b69d895fcbe98fc714b47057b0051387e05.tar.gz lphub-5fb47b69d895fcbe98fc714b47057b0051387e05.tar.bz2 lphub-5fb47b69d895fcbe98fc714b47057b0051387e05.zip | |
feat/rankings: fetch profiles faster and improvements (#234)v1.0.2
| -rw-r--r-- | rankings/.gitignore | 3 | ||||
| -rw-r--r-- | rankings/export.go | 20 | ||||
| -rw-r--r-- | rankings/fetch.go | 81 | ||||
| -rw-r--r-- | rankings/filter.go | 37 | ||||
| -rw-r--r-- | rankings/main.go | 37 | ||||
| -rw-r--r-- | rankings/prefetch.go | 8 |
6 files changed, 146 insertions, 40 deletions
diff --git a/rankings/.gitignore b/rankings/.gitignore index 764d23d..48d3792 100644 --- a/rankings/.gitignore +++ b/rankings/.gitignore | |||
| @@ -1,2 +1,3 @@ | |||
| 1 | .env | 1 | .env |
| 2 | output/ \ No newline at end of file | 2 | output/ |
| 3 | cache/ | ||
diff --git a/rankings/export.go b/rankings/export.go index 20dfebe..cdb9213 100644 --- a/rankings/export.go +++ b/rankings/export.go | |||
| @@ -2,20 +2,30 @@ package main | |||
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "encoding/json" | 4 | "encoding/json" |
| 5 | "log" | ||
| 5 | "os" | 6 | "os" |
| 6 | ) | 7 | ) |
| 7 | 8 | ||
| 8 | func exportAll(spRankings, mpRankings, overallRankings *[]*Player) { | 9 | func exportAll(spRankings, mpRankings, overallRankings []*Player) { |
| 9 | sp, _ := os.Create("./output/sp.json") | 10 | err := os.Mkdir("./output", 0775) |
| 10 | spRankingsOut, _ := json.Marshal(*spRankings) | 11 | if err != nil && !os.IsExist(err) { |
| 12 | log.Fatalln(err.Error()) | ||
| 13 | } | ||
| 14 | |||
| 15 | sp, err := os.Create("./output/sp.json") | ||
| 16 | if err != nil { | ||
| 17 | log.Fatalln(err.Error()) | ||
| 18 | } | ||
| 19 | |||
| 20 | spRankingsOut, _ := json.Marshal(spRankings) | ||
| 11 | sp.Write(spRankingsOut) | 21 | sp.Write(spRankingsOut) |
| 12 | sp.Close() | 22 | sp.Close() |
| 13 | mp, _ := os.Create("./output/mp.json") | 23 | mp, _ := os.Create("./output/mp.json") |
| 14 | mpRankingsOut, _ := json.Marshal(*mpRankings) | 24 | mpRankingsOut, _ := json.Marshal(mpRankings) |
| 15 | mp.Write(mpRankingsOut) | 25 | mp.Write(mpRankingsOut) |
| 16 | mp.Close() | 26 | mp.Close() |
| 17 | overall, _ := os.Create("./output/overall.json") | 27 | overall, _ := os.Create("./output/overall.json") |
| 18 | overallRankingsOut, _ := json.Marshal(*overallRankings) | 28 | overallRankingsOut, _ := json.Marshal(overallRankings) |
| 19 | overall.Write(overallRankingsOut) | 29 | overall.Write(overallRankingsOut) |
| 20 | overall.Close() | 30 | overall.Close() |
| 21 | } | 31 | } |
diff --git a/rankings/fetch.go b/rankings/fetch.go index ee5d5bb..cf04e81 100644 --- a/rankings/fetch.go +++ b/rankings/fetch.go | |||
| @@ -9,9 +9,10 @@ import ( | |||
| 9 | "net/http" | 9 | "net/http" |
| 10 | "os" | 10 | "os" |
| 11 | "strconv" | 11 | "strconv" |
| 12 | "strings" | ||
| 12 | ) | 13 | ) |
| 13 | 14 | ||
| 14 | func fetchLeaderboard(records *[]Record, overrides *map[string]map[string]int) *map[string]*Player { | 15 | func fetchLeaderboard(records []Record, overrides map[string]map[string]int, useCache bool) map[string]*Player { |
| 15 | log.Println("fetching leaderboard") | 16 | log.Println("fetching leaderboard") |
| 16 | players := map[string]*Player{} | 17 | players := map[string]*Player{} |
| 17 | // first init players map with records from portal gun and doors | 18 | // first init players map with records from portal gun and doors |
| @@ -20,8 +21,8 @@ func fetchLeaderboard(records *[]Record, overrides *map[string]map[string]int) * | |||
| 20 | end := 5000 | 21 | end := 5000 |
| 21 | 22 | ||
| 22 | for fetchAnotherPage { | 23 | for fetchAnotherPage { |
| 23 | portalGunEntries := fetchRecordsFromMap(47459, 0, 5000) | 24 | portalGunEntries := fetchRecordsFromMap(47459, 0, 5000, useCache) |
| 24 | fetchAnotherPage = portalGunEntries.needsAnotherPage(&(*records)[0]) | 25 | fetchAnotherPage = portalGunEntries.needsAnotherPage(&records[0]) |
| 25 | if fetchAnotherPage { | 26 | if fetchAnotherPage { |
| 26 | start = end + 1 | 27 | start = end + 1 |
| 27 | end = start + 5000 | 28 | end = start + 5000 |
| @@ -49,8 +50,8 @@ func fetchLeaderboard(records *[]Record, overrides *map[string]map[string]int) * | |||
| 49 | end = 5000 | 50 | end = 5000 |
| 50 | 51 | ||
| 51 | for fetchAnotherPage { | 52 | for fetchAnotherPage { |
| 52 | doorsEntries := fetchRecordsFromMap(47740, start, end) | 53 | doorsEntries := fetchRecordsFromMap(47740, start, end, useCache) |
| 53 | fetchAnotherPage = doorsEntries.needsAnotherPage(&(*records)[51]) | 54 | fetchAnotherPage = doorsEntries.needsAnotherPage(&records[51]) |
| 54 | if fetchAnotherPage { | 55 | if fetchAnotherPage { |
| 55 | start = end + 1 | 56 | start = end + 1 |
| 56 | end = start + 5000 | 57 | end = start + 5000 |
| @@ -83,7 +84,7 @@ func fetchLeaderboard(records *[]Record, overrides *map[string]map[string]int) * | |||
| 83 | } | 84 | } |
| 84 | } | 85 | } |
| 85 | 86 | ||
| 86 | for _, record := range *records { | 87 | for _, record := range records { |
| 87 | if record.MapID == 47459 || record.MapID == 47740 { | 88 | if record.MapID == 47459 || record.MapID == 47740 { |
| 88 | continue | 89 | continue |
| 89 | } | 90 | } |
| @@ -93,7 +94,7 @@ func fetchLeaderboard(records *[]Record, overrides *map[string]map[string]int) * | |||
| 93 | end := 5000 | 94 | end := 5000 |
| 94 | 95 | ||
| 95 | for fetchAnotherPage { | 96 | for fetchAnotherPage { |
| 96 | entries := fetchRecordsFromMap(record.MapID, start, end) | 97 | entries := fetchRecordsFromMap(record.MapID, start, end, useCache) |
| 97 | fetchAnotherPage = entries.needsAnotherPage(&record) | 98 | fetchAnotherPage = entries.needsAnotherPage(&record) |
| 98 | if fetchAnotherPage { | 99 | if fetchAnotherPage { |
| 99 | start = end + 1 | 100 | start = end + 1 |
| @@ -106,11 +107,11 @@ func fetchLeaderboard(records *[]Record, overrides *map[string]map[string]int) * | |||
| 106 | } | 107 | } |
| 107 | score := entry.Score | 108 | score := entry.Score |
| 108 | if entry.Score < record.MapWR { | 109 | if entry.Score < record.MapWR { |
| 109 | _, ok := (*overrides)[entry.SteamID] | 110 | _, ok := overrides[entry.SteamID] |
| 110 | if ok { | 111 | if ok { |
| 111 | _, ok := (*overrides)[entry.SteamID][strconv.Itoa(record.MapID)] | 112 | _, ok := overrides[entry.SteamID][strconv.Itoa(record.MapID)] |
| 112 | if ok { | 113 | if ok { |
| 113 | score = (*overrides)[entry.SteamID][strconv.Itoa(record.MapID)] | 114 | score = overrides[entry.SteamID][strconv.Itoa(record.MapID)] |
| 114 | } else { | 115 | } else { |
| 115 | continue // ban | 116 | continue // ban |
| 116 | } | 117 | } |
| @@ -136,28 +137,60 @@ func fetchLeaderboard(records *[]Record, overrides *map[string]map[string]int) * | |||
| 136 | } | 137 | } |
| 137 | 138 | ||
| 138 | } | 139 | } |
| 139 | return &players | 140 | return players |
| 140 | } | 141 | } |
| 141 | 142 | ||
| 142 | func fetchRecordsFromMap(mapID int, start int, end int) *Leaderboard { | 143 | func fetchRecordsFromMap(mapID int, start int, end int, useCache bool) *Leaderboard { |
| 143 | resp, err := http.Get(fmt.Sprintf("https://steamcommunity.com/stats/Portal2/leaderboards/%d?xml=1&start=%d&end=%d", mapID, start, end)) | 144 | var filename string |
| 145 | if useCache { | ||
| 146 | filename := fmt.Sprintf("./cache/lb_%d_%d_%d.xml", mapID, start, end) | ||
| 147 | log.Println("from cache", filename) | ||
| 148 | file, _ := os.ReadFile(filename) | ||
| 149 | if file != nil { | ||
| 150 | leaderboard := Leaderboard{} | ||
| 151 | err := xml.Unmarshal(file, &leaderboard) | ||
| 152 | if err != nil { | ||
| 153 | log.Fatalln("failed to unmarshal cache.", err.Error()) | ||
| 154 | } | ||
| 155 | return &leaderboard | ||
| 156 | } | ||
| 157 | } | ||
| 158 | |||
| 159 | url := fmt.Sprintf("https://steamcommunity.com/stats/Portal2/leaderboards/%d?xml=1&start=%d&end=%d", mapID, start, end) | ||
| 160 | resp, err := http.Get(url) | ||
| 161 | log.Println("fetched", url, ":", resp.StatusCode) | ||
| 144 | if err != nil { | 162 | if err != nil { |
| 145 | log.Fatalln(err.Error()) | 163 | log.Fatalln("failed to fetch leaderboard.", err.Error()) |
| 146 | } | 164 | } |
| 147 | respBytes, err := io.ReadAll(resp.Body) | 165 | respBytes, err := io.ReadAll(resp.Body) |
| 148 | if err != nil { | 166 | if err != nil { |
| 149 | log.Fatalln(err.Error()) | 167 | log.Fatalln("failed to read leadeboard body.", err.Error()) |
| 150 | } | 168 | } |
| 151 | leaderboard := Leaderboard{} | 169 | leaderboard := Leaderboard{} |
| 152 | err = xml.Unmarshal(respBytes, &leaderboard) | 170 | err = xml.Unmarshal(respBytes, &leaderboard) |
| 153 | if err != nil { | 171 | if err != nil { |
| 154 | log.Fatalln(err.Error()) | 172 | log.Println(string(respBytes)) |
| 173 | log.Fatalln("failed to unmarshal leaderboard.", err.Error()) | ||
| 155 | } | 174 | } |
| 175 | |||
| 176 | if useCache { | ||
| 177 | if err = os.WriteFile(filename, respBytes, 0644); err != nil { | ||
| 178 | log.Fatalln("failed write to file.", err.Error()) | ||
| 179 | } | ||
| 180 | } | ||
| 181 | |||
| 156 | return &leaderboard | 182 | return &leaderboard |
| 157 | } | 183 | } |
| 158 | 184 | ||
| 159 | func fetchPlayerInfo(player *Player) { | 185 | func fetchPlayerInfo(players []*Player) { |
| 160 | url := fmt.Sprintf("http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key=%s&steamids=%s", os.Getenv("API_KEY"), player.SteamID) | 186 | log.Println("fetching info for", len(players), "players") |
| 187 | |||
| 188 | ids := make([]string, len(players)) | ||
| 189 | for _, player := range players { | ||
| 190 | ids = append(ids, player.SteamID) | ||
| 191 | } | ||
| 192 | |||
| 193 | url := fmt.Sprintf("http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key=%s&steamids=%s", os.Getenv("API_KEY"), strings.Join(ids, ",")) | ||
| 161 | resp, err := http.Get(url) | 194 | resp, err := http.Get(url) |
| 162 | if err != nil { | 195 | if err != nil { |
| 163 | log.Fatalln(err.Error()) | 196 | log.Fatalln(err.Error()) |
| @@ -167,6 +200,7 @@ func fetchPlayerInfo(player *Player) { | |||
| 167 | log.Fatalln(err.Error()) | 200 | log.Fatalln(err.Error()) |
| 168 | } | 201 | } |
| 169 | type PlayerSummary struct { | 202 | type PlayerSummary struct { |
| 203 | SteamID string `json:"steamid"` | ||
| 170 | PersonaName string `json:"personaname"` | 204 | PersonaName string `json:"personaname"` |
| 171 | AvatarFull string `json:"avatarfull"` | 205 | AvatarFull string `json:"avatarfull"` |
| 172 | } | 206 | } |
| @@ -180,6 +214,13 @@ func fetchPlayerInfo(player *Player) { | |||
| 180 | if err := json.Unmarshal(body, &data); err != nil { | 214 | if err := json.Unmarshal(body, &data); err != nil { |
| 181 | log.Fatalln(err.Error()) | 215 | log.Fatalln(err.Error()) |
| 182 | } | 216 | } |
| 183 | player.AvatarLink = data.Response.Players[0].AvatarFull | 217 | |
| 184 | player.Username = data.Response.Players[0].PersonaName | 218 | for _, profile := range data.Response.Players { |
| 219 | for _, player := range players { | ||
| 220 | if player.SteamID == profile.SteamID { | ||
| 221 | player.AvatarLink = profile.AvatarFull | ||
| 222 | player.Username = profile.PersonaName | ||
| 223 | } | ||
| 224 | } | ||
| 225 | } | ||
| 185 | } | 226 | } |
diff --git a/rankings/filter.go b/rankings/filter.go index 1d7233b..2af7911 100644 --- a/rankings/filter.go +++ b/rankings/filter.go | |||
| @@ -2,11 +2,12 @@ package main | |||
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "log" | 4 | "log" |
| 5 | "math" | ||
| 5 | "sort" | 6 | "sort" |
| 6 | ) | 7 | ) |
| 7 | 8 | ||
| 8 | func filterRankings(spRankings, mpRankings, overallRankings *[]*Player, players *map[string]*Player) { | 9 | func filterRankings(spRankings, mpRankings, overallRankings *[]*Player, players map[string]*Player) { |
| 9 | for k, p := range *players { | 10 | for k, p := range players { |
| 10 | if p.SpIterations == 51 { | 11 | if p.SpIterations == 51 { |
| 11 | *spRankings = append(*spRankings, p) | 12 | *spRankings = append(*spRankings, p) |
| 12 | } | 13 | } |
| @@ -18,13 +19,14 @@ func filterRankings(spRankings, mpRankings, overallRankings *[]*Player, players | |||
| 18 | *overallRankings = append(*overallRankings, p) | 19 | *overallRankings = append(*overallRankings, p) |
| 19 | } | 20 | } |
| 20 | if p.SpIterations < 51 && p.MpIterations < 48 { | 21 | if p.SpIterations < 51 && p.MpIterations < 48 { |
| 21 | delete(*players, k) | 22 | delete(players, k) |
| 22 | } | 23 | } |
| 23 | } | 24 | } |
| 24 | 25 | ||
| 25 | log.Println("getting player summaries") | 26 | log.Println("getting player summaries for", len(players), "players") |
| 26 | for _, v := range *players { | 27 | |
| 27 | fetchPlayerInfo(v) | 28 | for _, chunk := range chunkMap(players, 100) { |
| 29 | fetchPlayerInfo(chunk) | ||
| 28 | } | 30 | } |
| 29 | 31 | ||
| 30 | log.Println("sorting the ranks") | 32 | log.Println("sorting the ranks") |
| @@ -91,3 +93,26 @@ func filterRankings(spRankings, mpRankings, overallRankings *[]*Player, players | |||
| 91 | (*overallRankings)[idx].OverallRank = rank | 93 | (*overallRankings)[idx].OverallRank = rank |
| 92 | } | 94 | } |
| 93 | } | 95 | } |
| 96 | |||
| 97 | func chunkMap[T any](m map[string]*T, chunkSize int) [][]*T { | ||
| 98 | chunks := make([][]*T, 0, int(math.Ceil(float64(len(m))/float64(chunkSize)))) | ||
| 99 | chunk := make([]*T, 0, chunkSize) | ||
| 100 | |||
| 101 | count := 0 | ||
| 102 | for _, player := range m { | ||
| 103 | chunk = append(chunk, player) | ||
| 104 | count++ | ||
| 105 | |||
| 106 | if count == chunkSize { | ||
| 107 | chunks = append(chunks, chunk) | ||
| 108 | chunk = make([]*T, 0, chunkSize) | ||
| 109 | count = 0 | ||
| 110 | } | ||
| 111 | } | ||
| 112 | |||
| 113 | if len(chunk) > 0 { | ||
| 114 | chunks = append(chunks, chunk) | ||
| 115 | } | ||
| 116 | |||
| 117 | return chunks | ||
| 118 | } | ||
diff --git a/rankings/main.go b/rankings/main.go index dfafb0c..552f058 100644 --- a/rankings/main.go +++ b/rankings/main.go | |||
| @@ -10,11 +10,33 @@ import ( | |||
| 10 | "github.com/robfig/cron/v3" | 10 | "github.com/robfig/cron/v3" |
| 11 | ) | 11 | ) |
| 12 | 12 | ||
| 13 | var useCache = false | ||
| 14 | |||
| 13 | func main() { | 15 | func main() { |
| 14 | err := godotenv.Load() | 16 | err := godotenv.Load() |
| 15 | if err != nil { | 17 | if err != nil { |
| 16 | log.Fatalln("Error loading .env file:", err.Error()) | 18 | log.Fatalln("Error loading .env file:", err.Error()) |
| 17 | } | 19 | } |
| 20 | |||
| 21 | runNow := false | ||
| 22 | for _, arg := range os.Args { | ||
| 23 | if arg == "-n" || arg == "--now" { | ||
| 24 | runNow = true | ||
| 25 | continue | ||
| 26 | } | ||
| 27 | if arg == "-c" || arg == "--cache" { | ||
| 28 | useCache = true | ||
| 29 | continue | ||
| 30 | } | ||
| 31 | } | ||
| 32 | |||
| 33 | useCache = useCache && runNow | ||
| 34 | |||
| 35 | if runNow { | ||
| 36 | run() | ||
| 37 | return | ||
| 38 | } | ||
| 39 | |||
| 18 | c := cron.New() | 40 | c := cron.New() |
| 19 | _, err = c.AddFunc("0 0 * * *", run) | 41 | _, err = c.AddFunc("0 0 * * *", run) |
| 20 | if err != nil { | 42 | if err != nil { |
| @@ -29,17 +51,24 @@ func main() { | |||
| 29 | 51 | ||
| 30 | func run() { | 52 | func run() { |
| 31 | log.Println("started job") | 53 | log.Println("started job") |
| 54 | |||
| 32 | records := readRecords() | 55 | records := readRecords() |
| 56 | log.Println("loaded", len(records), "records") | ||
| 57 | |||
| 33 | overrides := readOverrides() | 58 | overrides := readOverrides() |
| 34 | players := fetchLeaderboard(records, overrides) | 59 | log.Println("loaded", len(overrides), "player overrides") |
| 60 | |||
| 61 | players := fetchLeaderboard(records, overrides, useCache) | ||
| 35 | 62 | ||
| 36 | spRankings := []*Player{} | 63 | spRankings := []*Player{} |
| 37 | mpRankings := []*Player{} | 64 | mpRankings := []*Player{} |
| 38 | overallRankings := []*Player{} | 65 | overallRankings := []*Player{} |
| 39 | 66 | ||
| 40 | log.Println("filtering rankings") | 67 | log.Println("filtering rankings for", len(players), "players") |
| 41 | filterRankings(&spRankings, &mpRankings, &overallRankings, players) | 68 | filterRankings(&spRankings, &mpRankings, &overallRankings, players) |
| 42 | 69 | ||
| 43 | log.Println("exporting jsons") | 70 | log.Println("exporting jsons for", len(players), "players") |
| 44 | exportAll(&spRankings, &mpRankings, &overallRankings) | 71 | exportAll(spRankings, mpRankings, overallRankings) |
| 72 | |||
| 73 | log.Println("done") | ||
| 45 | } | 74 | } |
diff --git a/rankings/prefetch.go b/rankings/prefetch.go index 487a76f..a559b26 100644 --- a/rankings/prefetch.go +++ b/rankings/prefetch.go | |||
| @@ -7,7 +7,7 @@ import ( | |||
| 7 | "os" | 7 | "os" |
| 8 | ) | 8 | ) |
| 9 | 9 | ||
| 10 | func readRecords() *[]Record { | 10 | func readRecords() []Record { |
| 11 | recordsFile, err := os.Open("./input/records.json") | 11 | recordsFile, err := os.Open("./input/records.json") |
| 12 | if err != nil { | 12 | if err != nil { |
| 13 | log.Fatalln(err.Error()) | 13 | log.Fatalln(err.Error()) |
| @@ -22,10 +22,10 @@ func readRecords() *[]Record { | |||
| 22 | if err != nil { | 22 | if err != nil { |
| 23 | log.Fatalln(err.Error()) | 23 | log.Fatalln(err.Error()) |
| 24 | } | 24 | } |
| 25 | return &records | 25 | return records |
| 26 | } | 26 | } |
| 27 | 27 | ||
| 28 | func readOverrides() *map[string]map[string]int { | 28 | func readOverrides() map[string]map[string]int { |
| 29 | overridesFile, err := os.Open("./input/overrides.json") | 29 | overridesFile, err := os.Open("./input/overrides.json") |
| 30 | if err != nil { | 30 | if err != nil { |
| 31 | log.Fatalln(err.Error()) | 31 | log.Fatalln(err.Error()) |
| @@ -40,5 +40,5 @@ func readOverrides() *map[string]map[string]int { | |||
| 40 | if err != nil { | 40 | if err != nil { |
| 41 | log.Fatalln(err.Error()) | 41 | log.Fatalln(err.Error()) |
| 42 | } | 42 | } |
| 43 | return &overrides | 43 | return overrides |
| 44 | } | 44 | } |