203 lines
5.4 KiB
Go
203 lines
5.4 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/runescape-wiki/rsw/internal/prices"
|
|
"github.com/runescape-wiki/rsw/internal/render"
|
|
"github.com/runescape-wiki/rsw/internal/wiki"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func newPriceCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "price <item name>",
|
|
Short: "Look up Grand Exchange prices for an item",
|
|
Long: `Fetches real-time GE price data for an item: current buy/sell prices,
|
|
trade volume, and recent trend.
|
|
|
|
With --ironman, shows alch values and vendor prices instead of GE data.
|
|
|
|
Examples:
|
|
rsw osrs price "abyssal whip"
|
|
rsw rs3 price "blue partyhat"
|
|
rsw osrs price "dragon bones" --ironman`,
|
|
Args: cobra.MinimumNArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
name := args[0]
|
|
|
|
if Game() == "rs3" {
|
|
return runRS3Price(name)
|
|
}
|
|
|
|
priceClient := prices.NewClient(GamePriceBaseURL())
|
|
mc := prices.NewMappingCache(priceClient, Game())
|
|
|
|
if err := mc.Load(); err != nil {
|
|
return fmt.Errorf("failed to load item mapping: %w", err)
|
|
}
|
|
|
|
item := mc.LookupByName(name)
|
|
if item == nil {
|
|
matches := mc.SearchByName(name)
|
|
if len(matches) == 0 {
|
|
return fmt.Errorf("no item found matching %q", name)
|
|
}
|
|
if len(matches) > 1 {
|
|
md := render.New()
|
|
md.H2("Multiple items found")
|
|
md.P("Did you mean one of these?")
|
|
shown := 0
|
|
for _, m := range matches {
|
|
md.Bullet(m.Name)
|
|
shown++
|
|
if shown >= 15 {
|
|
md.P(fmt.Sprintf("...and %d more", len(matches)-15))
|
|
break
|
|
}
|
|
}
|
|
fmt.Print(md.String())
|
|
return nil
|
|
}
|
|
item = matches[0]
|
|
}
|
|
|
|
md := render.New()
|
|
md.H1(item.Name)
|
|
md.KV("Examine", item.Examine)
|
|
md.KV("Members", fmt.Sprintf("%v", item.Members))
|
|
md.KV("Store value", render.FormatGP(item.Value))
|
|
|
|
if item.HighAlch != nil {
|
|
md.KV("High Alch", render.FormatGP(*item.HighAlch))
|
|
}
|
|
if item.LowAlch != nil {
|
|
md.KV("Low Alch", render.FormatGP(*item.LowAlch))
|
|
}
|
|
md.Newline()
|
|
|
|
if Ironman() {
|
|
md.H2("Ironman Info")
|
|
md.P("*GE trading unavailable — showing self-sufficient values.*")
|
|
md.KV("Store value", render.FormatGP(item.Value))
|
|
if item.HighAlch != nil {
|
|
md.KV("High Alch value", render.FormatGP(*item.HighAlch))
|
|
}
|
|
md.P("Use `rsw item` for drop sources and shop locations.")
|
|
} else {
|
|
latest, err := priceClient.GetLatestForItem(item.ID)
|
|
if err != nil {
|
|
md.P("*Price data unavailable.*")
|
|
} else {
|
|
md.H2("Grand Exchange Price")
|
|
if latest.High != nil {
|
|
md.KV("Instant buy", render.FormatGP(*latest.High))
|
|
if latest.HighTime != nil {
|
|
md.KV("Last buy", timeAgo(*latest.HighTime))
|
|
}
|
|
}
|
|
if latest.Low != nil {
|
|
md.KV("Instant sell", render.FormatGP(*latest.Low))
|
|
if latest.LowTime != nil {
|
|
md.KV("Last sell", timeAgo(*latest.LowTime))
|
|
}
|
|
}
|
|
if item.Limit != nil {
|
|
md.KV("Buy limit", render.FormatNumber(*item.Limit))
|
|
}
|
|
md.Newline()
|
|
}
|
|
|
|
hourly, err := priceClient.Get1Hour(item.ID)
|
|
if err == nil {
|
|
md.H2("Recent Activity (1h)")
|
|
if hourly.AvgHighPrice != nil {
|
|
md.KV("Avg buy price", render.FormatGP(*hourly.AvgHighPrice))
|
|
}
|
|
md.KV("Buy volume", render.FormatNumber(hourly.HighVolume))
|
|
if hourly.AvgLowPrice != nil {
|
|
md.KV("Avg sell price", render.FormatGP(*hourly.AvgLowPrice))
|
|
}
|
|
md.KV("Sell volume", render.FormatNumber(hourly.LowVolume))
|
|
md.Newline()
|
|
}
|
|
}
|
|
|
|
fmt.Print(md.String())
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func runRS3Price(name string) error {
|
|
wikiClient := wiki.NewClient(GameBaseURL())
|
|
|
|
results, err := wikiClient.Search(name, 5)
|
|
if err != nil {
|
|
return fmt.Errorf("search failed: %w", err)
|
|
}
|
|
if len(results) == 0 {
|
|
return fmt.Errorf("no wiki page found for %q", name)
|
|
}
|
|
|
|
pageTitle := results[0].Title
|
|
exchangeItem, err := wikiClient.GetExchangeModule(pageTitle)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to look up exchange data: %w", err)
|
|
}
|
|
if exchangeItem == nil {
|
|
return fmt.Errorf("%q is not tradeable on the Grand Exchange", pageTitle)
|
|
}
|
|
|
|
rs3Client := prices.NewRS3Client()
|
|
detail, err := rs3Client.GetDetail(exchangeItem.ItemID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to fetch RS3 price data: %w", err)
|
|
}
|
|
|
|
md := render.New()
|
|
md.H1(detail.Name)
|
|
md.KV("Examine", exchangeItem.Examine)
|
|
md.KV("Members", fmt.Sprintf("%v", exchangeItem.Members))
|
|
if exchangeItem.Limit > 0 {
|
|
md.KV("Buy limit", render.FormatNumber(exchangeItem.Limit))
|
|
}
|
|
md.KV("Store value", render.FormatGP(exchangeItem.Value))
|
|
md.Newline()
|
|
|
|
md.H2("Grand Exchange Price")
|
|
md.KV("Current price", detail.CurrentPrice+" gp")
|
|
if detail.TodayPrice != 0 {
|
|
md.KV("Today's change", render.FormatGP(detail.TodayPrice))
|
|
} else {
|
|
md.KV("Today's change", "0 gp")
|
|
}
|
|
md.KV("30-day change", fmt.Sprintf("%s (%s)", detail.Day30Change, detail.Day30Trend))
|
|
md.KV("90-day change", fmt.Sprintf("%s (%s)", detail.Day90Change, detail.Day90Trend))
|
|
md.KV("180-day change", fmt.Sprintf("%s (%s)", detail.Day180Change, detail.Day180Trend))
|
|
md.Newline()
|
|
|
|
fmt.Print(md.String())
|
|
return nil
|
|
}
|
|
|
|
func timeAgo(unixTime int64) string {
|
|
t := time.Unix(unixTime, 0)
|
|
d := time.Since(t)
|
|
switch {
|
|
case d < time.Minute:
|
|
return "just now"
|
|
case d < time.Hour:
|
|
return fmt.Sprintf("%d min ago", int(d.Minutes()))
|
|
case d < 24*time.Hour:
|
|
return fmt.Sprintf("%d hours ago", int(d.Hours()))
|
|
default:
|
|
return fmt.Sprintf("%d days ago", int(d.Hours()/24))
|
|
}
|
|
}
|
|
|
|
func init() {
|
|
RegisterCommand(newPriceCmd)
|
|
}
|