diff --git a/esi_sso.go b/esi_sso.go index 660df59..073661b 100644 --- a/esi_sso.go +++ b/esi_sso.go @@ -50,6 +50,9 @@ type ESISSO struct { server *http.Server db *gorm.DB + + nameCacheOnce sync.Once + nameToID map[string]int64 // lowercased name -> id } type SolarSystem struct { @@ -556,172 +559,58 @@ func parseTokenCharacter(jwt string) (name string, id int64) { return } -// ResolveSystemIDByName ... DB-first then ESI fallbacks -func (s *ESISSO) ResolveSystemIDByName(ctx context.Context, name string) (int64, error) { - name = strings.TrimSpace(name) - if name == "" { - return 0, errors.New("empty system name") - } - if s.db != nil { - var sys SolarSystem - if err := s.db.Where("solarSystemName = ?", name).First(&sys).Error; err == nil && sys.SolarSystemID != 0 { - fmt.Printf("DB: resolved %q -> %d\n", name, sys.SolarSystemID) - return sys.SolarSystemID, nil - } - } - // Fallbacks (universe/ids -> strict search -> non-strict + names) - type idsReq struct{ Names []string `json:"names"` } - body, _ := json.Marshal(idsReq{Names: []string{name}}) - idsURL := esiBase + "/v3/universe/ids/?datasource=tranquility" - fmt.Printf("ESI: resolve system id via universe/ids for name=%q\n", name) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, idsURL, strings.NewReader(string(body))) - if err == nil { - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json") - resp, errDo := http.DefaultClient.Do(req) - if errDo == nil { - defer resp.Body.Close() - if resp.StatusCode == http.StatusOK { - var idsResp struct{ Systems []struct{ ID int64 `json:"id"`; Name string `json:"name"` } `json:"systems"` } - if err := json.NewDecoder(resp.Body).Decode(&idsResp); err == nil { - if len(idsResp.Systems) > 0 { - fmt.Printf("ESI: universe/ids hit: %s -> %d\n", idsResp.Systems[0].Name, idsResp.Systems[0].ID) - return idsResp.Systems[0].ID, nil - } - } +// ensureNameCache loads a lowercase name->id map from the local DB once +func (s *ESISSO) ensureNameCache() error { + var err error + s.nameCacheOnce.Do(func() { + cache := make(map[string]int64, 50000) + if s.db != nil { + var rows []SolarSystem + // Only select required columns + if e := s.db.Select("solarSystemID, solarSystemName").Find(&rows).Error; e != nil { + err = e + return + } + for _, r := range rows { + cache[strings.ToLower(r.SolarSystemName)] = r.SolarSystemID } } - } - q := url.Values{} - q.Set("categories", "solar_system") - q.Set("search", name) - q.Set("strict", "true") - searchURL := esiBase + "/v3/search/?" + q.Encode() - fmt.Printf("ESI: strict search for %q\n", name) - req2, err := http.NewRequestWithContext(ctx, http.MethodGet, searchURL, nil) - if err != nil { - return 0, err - } - req2.Header.Set("Accept", "application/json") - resp2, err := http.DefaultClient.Do(req2) - if err == nil { - defer resp2.Body.Close() - if resp2.StatusCode == http.StatusOK { - var payload struct{ SolarSystem []int64 `json:"solar_system"` } - if err := json.NewDecoder(resp2.Body).Decode(&payload); err == nil && len(payload.SolarSystem) > 0 { - fmt.Printf("ESI: strict search hit: %d\n", payload.SolarSystem[0]) - return payload.SolarSystem[0], nil - } - } - } - q.Set("strict", "false") - searchURL2 := esiBase + "/v3/search/?" + q.Encode() - fmt.Printf("ESI: non-strict search for %q\n", name) - req3, err := http.NewRequestWithContext(ctx, http.MethodGet, searchURL2, nil) - if err != nil { - return 0, err - } - req3.Header.Set("Accept", "application/json") - resp3, err := http.DefaultClient.Do(req3) - if err != nil { - return 0, err - } - defer resp3.Body.Close() - if resp3.StatusCode != http.StatusOK { - b, _ := io.ReadAll(resp3.Body) - return 0, fmt.Errorf("search failed: %s: %s", resp3.Status, string(b)) - } - var payload struct{ SolarSystem []int64 `json:"solar_system"` } - if err := json.NewDecoder(resp3.Body).Decode(&payload); err != nil || len(payload.SolarSystem) == 0 { - return 0, fmt.Errorf("system not found: %s", name) - } - if len(payload.SolarSystem) == 1 { - fmt.Printf("ESI: non-strict search single hit: %d\n", payload.SolarSystem[0]) - return payload.SolarSystem[0], nil - } - ids := payload.SolarSystem - namesURL := esiBase + "/v3/universe/names/?datasource=tranquility" - idsBody, _ := json.Marshal(ids) - req4, err := http.NewRequestWithContext(ctx, http.MethodPost, namesURL, strings.NewReader(string(idsBody))) - if err != nil { - return 0, err - } - req4.Header.Set("Content-Type", "application/json") - req4.Header.Set("Accept", "application/json") - resp4, err := http.DefaultClient.Do(req4) - if err != nil { - return 0, err - } - defer resp4.Body.Close() - if resp4.StatusCode != http.StatusOK { - b, _ := io.ReadAll(resp4.Body) - return 0, fmt.Errorf("names lookup failed: %s: %s", resp4.Status, string(b)) - } - var namesResp []struct{ Category string `json:"category"`; ID int64 `json:"id"`; Name string `json:"name"` } - if err := json.NewDecoder(resp4.Body).Decode(&namesResp); err != nil { - return 0, err - } - lower := strings.ToLower(name) - for _, n := range namesResp { - if n.Category == "solar_system" && strings.ToLower(n.Name) == lower { - fmt.Printf("ESI: names resolved exact: %s -> %d\n", n.Name, n.ID) - return n.ID, nil - } - } - fmt.Printf("ESI: names resolved fallback: returning %d for %q\n", namesResp[0].ID, name) - return namesResp[0].ID, nil + s.nameToID = cache + }) + return err } -// ResolveSystemIDsByNames returns IDs in the same order as names. Missing entries error. +// ResolveSystemIDByName resolves using ONLY the local DB cache (case-insensitive) +func (s *ESISSO) ResolveSystemIDByName(ctx context.Context, name string) (int64, error) { + if err := s.ensureNameCache(); err != nil { + return 0, err + } + key := strings.ToLower(strings.TrimSpace(name)) + if id, ok := s.nameToID[key]; ok { + return id, nil + } + return 0, fmt.Errorf("system not found in local DB: %s", name) +} + +// ResolveSystemIDsByNames returns IDs in the same order as names using ONLY the local DB cache func (s *ESISSO) ResolveSystemIDsByNames(ctx context.Context, names []string) ([]int64, error) { - ordered := make([]int64, len(names)) - if len(names) == 0 { - return ordered, nil + if err := s.ensureNameCache(); err != nil { + return nil, err } - // DB first - nameLower := make([]string, len(names)) - for i, n := range names { nameLower[i] = strings.TrimSpace(n) } - found := map[string]int64{} - if s.db != nil { - var rows []SolarSystem - if err := s.db.Where("solarSystemName IN ?", nameLower).Find(&rows).Error; err == nil { - for _, r := range rows { - found[r.SolarSystemName] = r.SolarSystemID - } - } - } - // Collect missing for ESI batch + out := make([]int64, len(names)) missing := []string{} - for _, n := range nameLower { - if _, ok := found[n]; !ok { + for i, n := range names { + key := strings.ToLower(strings.TrimSpace(n)) + if id, ok := s.nameToID[key]; ok { + out[i] = id + } else { missing = append(missing, n) } } if len(missing) > 0 { - idsURL := esiBase + "/v3/universe/ids/?datasource=tranquility" - body, _ := json.Marshal(struct{ Names []string `json:"names"` }{Names: missing}) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, idsURL, strings.NewReader(string(body))) - if err != nil { return nil, err } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json") - resp, err := http.DefaultClient.Do(req) - if err != nil { return nil, err } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - b, _ := io.ReadAll(resp.Body) - return nil, fmt.Errorf("universe/ids failed: %s: %s", resp.Status, string(b)) - } - var idsResp struct{ Systems []struct{ ID int64 `json:"id"`; Name string `json:"name"` } `json:"systems"` } - if err := json.NewDecoder(resp.Body).Decode(&idsResp); err != nil { return nil, err } - for _, it := range idsResp.Systems { found[it.Name] = it.ID } + return nil, fmt.Errorf("systems not found in local DB: %s", strings.Join(missing, ", ")) } - // Build ordered list - for i, n := range nameLower { - id, ok := found[n] - if !ok { return nil, fmt.Errorf("system not found: %s", n) } - ordered[i] = id - } - return ordered, nil + return out, nil } // PostRouteForAll clears route and posts vias then destination last