mirror of
https://github.com/tiennm99/loldle.git
synced 2026-10-11 12:19:08 +00:00
4-stage pipeline: fetch ddragon champions, enrich with release dates (wiki scrape), regions (Universe API), and lanes (wiki scrape). Concurrent gender detection via goroutines with semaphore.
159 lines
4.2 KiB
Go
159 lines
4.2 KiB
Go
package parser
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// ddragonResponse represents the top-level ddragon championFull.json response.
|
|
type ddragonResponse struct {
|
|
Data map[string]ddragonChampion `json:"data"`
|
|
}
|
|
|
|
type ddragonChampion struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Title string `json:"title"`
|
|
Partype string `json:"partype"`
|
|
Tags []string `json:"tags"`
|
|
Skins []json.RawMessage `json:"skins"`
|
|
Image interface{} `json:"image"`
|
|
Stats struct {
|
|
AttackRange float64 `json:"attackrange"`
|
|
} `json:"stats"`
|
|
}
|
|
|
|
// ChampionResult holds the enriched data for a single champion.
|
|
type ChampionResult struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Title string `json:"title"`
|
|
Resource string `json:"resource"`
|
|
Genre string `json:"genre"`
|
|
SkinCount int `json:"skinCount"`
|
|
Image interface{} `json:"image"`
|
|
Gender string `json:"gender"`
|
|
AttackType string `json:"attackType"`
|
|
ReleaseDate int `json:"releaseDate,omitempty"`
|
|
Region string `json:"region,omitempty"`
|
|
Lane string `json:"lane,omitempty"`
|
|
}
|
|
|
|
// ParseChampions fetches base champion data from ddragon and detects gender concurrently.
|
|
func ParseChampions(version string) ([]ChampionResult, error) {
|
|
url := fmt.Sprintf("https://ddragon.leagueoflegends.com/cdn/%s/data/en_US/championFull.json", version)
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fetching ddragon: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var ddResp ddragonResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&ddResp); err != nil {
|
|
return nil, fmt.Errorf("decoding ddragon: %w", err)
|
|
}
|
|
|
|
// Fetch gender concurrently for all champions.
|
|
genderMap := make(map[string]string, len(ddResp.Data))
|
|
var mu sync.Mutex
|
|
var wg sync.WaitGroup
|
|
sem := make(chan struct{}, 10) // limit to 10 concurrent requests
|
|
|
|
for id := range ddResp.Data {
|
|
wg.Add(1)
|
|
go func(champID string) {
|
|
defer wg.Done()
|
|
sem <- struct{}{}
|
|
defer func() { <-sem }()
|
|
|
|
gender := detectGender(champID)
|
|
mu.Lock()
|
|
genderMap[champID] = gender
|
|
mu.Unlock()
|
|
}(id)
|
|
}
|
|
wg.Wait()
|
|
|
|
// Map data to result slice.
|
|
results := make([]ChampionResult, 0, len(ddResp.Data))
|
|
for _, champ := range ddResp.Data {
|
|
attackType := "range"
|
|
if champ.Stats.AttackRange < 500 {
|
|
attackType = "close"
|
|
}
|
|
|
|
results = append(results, ChampionResult{
|
|
ID: champ.ID,
|
|
Name: champ.Name,
|
|
Title: champ.Title,
|
|
Resource: champ.Partype,
|
|
Genre: strings.Join(champ.Tags, ","),
|
|
SkinCount: len(champ.Skins),
|
|
Image: champ.Image,
|
|
Gender: genderMap[champ.ID],
|
|
AttackType: attackType,
|
|
})
|
|
fmt.Printf("Data fetched for %s\n", champ.Name)
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
// universeChampionResponse represents the biography response from the Universe API.
|
|
type universeChampionResponse struct {
|
|
Champion struct {
|
|
Biography struct {
|
|
Full string `json:"full"`
|
|
} `json:"biography"`
|
|
} `json:"champion"`
|
|
}
|
|
|
|
// detectGender infers champion gender from biography pronoun frequency.
|
|
func detectGender(championID string) string {
|
|
// Special case: Renata's URL slug differs from her ID.
|
|
urlID := strings.ToLower(championID)
|
|
if urlID == "renata" {
|
|
urlID = "renataglasc"
|
|
}
|
|
|
|
url := fmt.Sprintf("https://universe-meeps.leagueoflegends.com/v1/en_us/champions/%s/index.json", urlID)
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
resp, err := client.Get(url)
|
|
if err != nil {
|
|
return "divers"
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var ucResp universeChampionResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&ucResp); err != nil {
|
|
return "divers"
|
|
}
|
|
|
|
words := strings.Fields(strings.ToLower(ucResp.Champion.Biography.Full))
|
|
maleKeywords := map[string]bool{"he": true, "him": true, "his": true}
|
|
femaleKeywords := map[string]bool{"she": true, "her": true, "hers": true}
|
|
|
|
var maleCount, femaleCount int
|
|
for _, w := range words {
|
|
if maleKeywords[w] {
|
|
maleCount++
|
|
}
|
|
if femaleKeywords[w] {
|
|
femaleCount++
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case maleCount > femaleCount:
|
|
return "male"
|
|
case femaleCount > maleCount:
|
|
return "female"
|
|
default:
|
|
return "divers"
|
|
}
|
|
}
|