package prices import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "time" ) const cacheTTL = 24 * time.Hour // MappingCache provides a local cache of the item ID ↔ name mapping. type MappingCache struct { client *Client items []MappingItem byID map[int]*MappingItem byName map[string]*MappingItem cacheDir string game string } // NewMappingCache creates a mapping cache backed by the given client. // The game parameter scopes the cache file (e.g. "osrs" or "rs3"). func NewMappingCache(client *Client, game string) *MappingCache { home, _ := os.UserHomeDir() return &MappingCache{ client: client, byID: make(map[int]*MappingItem), byName: make(map[string]*MappingItem), cacheDir: filepath.Join(home, ".rsw", "cache"), game: game, } } // Load populates the cache, reading from disk if fresh or fetching from API. func (mc *MappingCache) Load() error { // Try disk cache first if mc.loadFromDisk() { return nil } items, err := mc.client.GetMapping() if err != nil { return fmt.Errorf("fetching mapping: %w", err) } mc.items = items mc.index() mc.saveToDisk() return nil } // LookupByName finds an item by exact name (case-insensitive). func (mc *MappingCache) LookupByName(name string) *MappingItem { return mc.byName[strings.ToLower(name)] } // SearchByName finds items whose name contains the query (case-insensitive). func (mc *MappingCache) SearchByName(query string) []*MappingItem { query = strings.ToLower(query) var results []*MappingItem for i := range mc.items { if strings.Contains(strings.ToLower(mc.items[i].Name), query) { results = append(results, &mc.items[i]) } } return results } // LookupByID finds an item by its ID. func (mc *MappingCache) LookupByID(id int) *MappingItem { return mc.byID[id] } func (mc *MappingCache) index() { mc.byID = make(map[int]*MappingItem, len(mc.items)) mc.byName = make(map[string]*MappingItem, len(mc.items)) for i := range mc.items { mc.byID[mc.items[i].ID] = &mc.items[i] mc.byName[strings.ToLower(mc.items[i].Name)] = &mc.items[i] } } func (mc *MappingCache) cachePath() string { filename := mc.game + "_mapping.json" return filepath.Join(mc.cacheDir, filename) } func (mc *MappingCache) loadFromDisk() bool { path := mc.cachePath() info, err := os.Stat(path) if err != nil { return false } if time.Since(info.ModTime()) > cacheTTL { return false } data, err := os.ReadFile(path) if err != nil { return false } var items []MappingItem if err := json.Unmarshal(data, &items); err != nil { return false } mc.items = items mc.index() return true } func (mc *MappingCache) saveToDisk() { _ = os.MkdirAll(mc.cacheDir, 0755) data, err := json.Marshal(mc.items) if err != nil { return } _ = os.WriteFile(mc.cachePath(), data, 0644) }