Files
nuzlocke-tracker/tools/fetch-pokeapi/main.go
Julian Tabel 0bf628157f Add Go-based PokeAPI fetch tool
Replaces the Python fetch_pokeapi.py script with a Go tool that crawls
a local PokeAPI instance and writes seed JSON files. Supports caching
and special encounter definitions via JSON config.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 19:44:05 +01:00

257 lines
7.2 KiB
Go

package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
)
// Config structs for version_groups.json
type VersionGroupInfo struct {
Versions []string `json:"versions"`
Generation int `json:"generation"`
Region string `json:"region"`
RegionID int `json:"region_id"`
ExtraRegions []int `json:"extra_regions"`
Games map[string]GameInfo `json:"games"`
}
type GameInfo struct {
Name string `json:"name"`
Slug string `json:"slug"`
ReleaseYear int `json:"release_year"`
Color *string `json:"color"`
}
// Config structs for route_order.json
type RouteOrderFile struct {
Routes map[string][]string `json:"routes"`
Aliases map[string]string `json:"aliases"`
}
// Config structs for special_encounters.json
type SpecialEncountersFile struct {
Encounters map[string]map[string][]EncounterOutput `json:"encounters"`
Aliases map[string]string `json:"aliases"`
}
func getSpecialEncounters(se *SpecialEncountersFile, vgKey string) map[string][]EncounterOutput {
if se == nil {
return nil
}
if data, ok := se.Encounters[vgKey]; ok {
return data
}
if alias, ok := se.Aliases[vgKey]; ok {
if data, ok := se.Encounters[alias]; ok {
return data
}
}
return nil
}
func main() {
clearCache := flag.Bool("clear-cache", false, "Delete cached API responses before fetching")
flag.Parse()
pokeapiURL := os.Getenv("POKEAPI_URL")
if pokeapiURL == "" {
pokeapiURL = "http://localhost:8000/api/v2"
}
// Resolve paths relative to this tool's location or use the standard layout
seedsDir := findSeedsDir()
dataDir := filepath.Join(seedsDir, "data")
cacheDir := filepath.Join(seedsDir, ".pokeapi_cache")
client := NewClient(pokeapiURL, cacheDir, 50)
if *clearCache {
if err := client.ClearCache(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: could not clear cache: %v\n", err)
} else {
fmt.Println("Cleared API cache.")
}
}
ctx := context.Background()
// Connectivity check
fmt.Printf("Connecting to PokeAPI at %s...\n", pokeapiURL)
if _, err := client.Get(ctx, "pokemon-species/1"); err != nil {
fmt.Fprintf(os.Stderr, "Error: Cannot connect to PokeAPI at %s\n %v\nStart the local PokeAPI server or set POKEAPI_URL.\n", pokeapiURL, err)
os.Exit(1)
}
// Load configs
versionGroups, err := loadJSON[map[string]VersionGroupInfo](filepath.Join(seedsDir, "version_groups.json"))
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading version_groups.json: %v\n", err)
os.Exit(1)
}
routeOrder, err := loadRouteOrder(filepath.Join(seedsDir, "route_order.json"))
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading route_order.json: %v\n", err)
os.Exit(1)
}
specialEnc, err := loadJSON[SpecialEncountersFile](filepath.Join(seedsDir, "special_encounters.json"))
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: could not load special_encounters.json: %v\n", err)
// Continue without special encounters
}
if err := os.MkdirAll(dataDir, 0o755); err != nil {
fmt.Fprintf(os.Stderr, "Error creating data dir: %v\n", err)
os.Exit(1)
}
// Build games.json
var games []GameOutput
for _, vgInfo := range *versionGroups {
for _, gameInfo := range vgInfo.Games {
games = append(games, GameOutput{
Name: gameInfo.Name,
Slug: gameInfo.Slug,
Generation: vgInfo.Generation,
Region: vgInfo.Region,
ReleaseYear: gameInfo.ReleaseYear,
Color: gameInfo.Color,
})
}
}
writeJSON(filepath.Join(dataDir, "games.json"), games)
fmt.Printf("Wrote %d games to games.json\n", len(games))
// Process each version
pokeIDCollector := NewPokeIDCollector()
for vgKey, vgInfo := range *versionGroups {
for _, verName := range vgInfo.Versions {
routes, err := processVersion(ctx, client, verName, vgInfo, vgKey, routeOrder, specialEnc, pokeIDCollector)
if err != nil {
fmt.Fprintf(os.Stderr, "Error processing %s: %v\n", verName, err)
os.Exit(1)
}
writeJSON(filepath.Join(dataDir, verName+".json"), routes)
}
}
// Fetch all species data (reused for pokemon discovery + evolutions)
speciesData, err := fetchAllSpecies(ctx, client)
if err != nil {
fmt.Fprintf(os.Stderr, "Error fetching species: %v\n", err)
os.Exit(1)
}
// Fetch all Pokemon (base + all forms)
pokemonList, err := fetchAllPokemon(ctx, client, speciesData, pokeIDCollector.IDs())
if err != nil {
fmt.Fprintf(os.Stderr, "Error fetching pokemon: %v\n", err)
os.Exit(1)
}
writeJSON(filepath.Join(dataDir, "pokemon.json"), pokemonList)
fmt.Printf("\nWrote %d Pokemon to pokemon.json\n", len(pokemonList))
// Build set of all seeded PokeAPI IDs for evolution filtering
allSeededDex := make(map[int]bool)
for _, p := range pokemonList {
allSeededDex[p.PokeAPIID] = true
}
// Fetch evolution chains
evolutions, err := fetchEvolutionData(ctx, client, speciesData, allSeededDex)
if err != nil {
fmt.Fprintf(os.Stderr, "Error fetching evolutions: %v\n", err)
os.Exit(1)
}
evolutions, err = applyEvolutionOverrides(evolutions, filepath.Join(dataDir, "evolution_overrides.json"))
if err != nil {
fmt.Fprintf(os.Stderr, "Error applying evolution overrides: %v\n", err)
os.Exit(1)
}
writeJSON(filepath.Join(dataDir, "evolutions.json"), evolutions)
fmt.Printf("\nWrote %d evolution pairs to evolutions.json\n", len(evolutions))
fmt.Println("\nDone! JSON files written to seeds/data/")
fmt.Println("Review route ordering and curate as needed.")
}
// findSeedsDir locates the backend/src/app/seeds directory.
func findSeedsDir() string {
// Try relative to CWD (from repo root)
candidates := []string{
"backend/src/app/seeds",
"../../backend/src/app/seeds", // from tools/fetch-pokeapi/
}
for _, c := range candidates {
if _, err := os.Stat(filepath.Join(c, "version_groups.json")); err == nil {
abs, _ := filepath.Abs(c)
return abs
}
}
// Fallback: try to find from executable location
exe, _ := os.Executable()
exeDir := filepath.Dir(exe)
rel := filepath.Join(exeDir, "../../backend/src/app/seeds")
if _, err := os.Stat(filepath.Join(rel, "version_groups.json")); err == nil {
abs, _ := filepath.Abs(rel)
return abs
}
// Default
abs, _ := filepath.Abs("backend/src/app/seeds")
return abs
}
func loadJSON[T any](path string) (*T, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var result T
if err := json.Unmarshal(data, &result); err != nil {
return nil, err
}
return &result, nil
}
func loadRouteOrder(path string) (map[string][]string, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var rof RouteOrderFile
if err := json.Unmarshal(data, &rof); err != nil {
return nil, err
}
routes := make(map[string][]string)
for k, v := range rof.Routes {
routes[k] = v
}
for alias, target := range rof.Aliases {
routes[alias] = routes[target]
}
return routes, nil
}
func writeJSON(path string, data interface{}) {
content, err := json.MarshalIndent(data, "", " ")
if err != nil {
fmt.Fprintf(os.Stderr, "Error marshaling JSON for %s: %v\n", path, err)
os.Exit(1)
}
content = append(content, '\n')
if err := os.WriteFile(path, content, 0o644); err != nil {
fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", path, err)
os.Exit(1)
}
fmt.Printf(" -> %s\n", path)
}