| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- // Command cheapo lists and watches free game offers (currently: Epic Games Store).
- //
- // Data is read from the public storefront backend, no browser and no login
- // needed. Reference implementation: vogler/free-games-claimer.
- package main
- import (
- "encoding/json"
- "fmt"
- "os"
- "os/signal"
- "syscall"
- "time"
- "github.com/spf13/cobra"
- "gogs.netdome.biz/paul/cheapo/internal/epic"
- "gogs.netdome.biz/paul/cheapo/internal/watch"
- )
- var (
- flagCountry string
- flagLocale string
- flagJSON bool
- )
- func main() {
- root := &cobra.Command{
- Use: "cheapo",
- Short: "cheapo — free games lister (Go edition)",
- Long: `cheapo lists and watches free game offers (currently: Epic Games Store).
- Data is read from the public storefront backend, no browser and no login needed.
- Reference: https://github.com/vogler/free-games-claimer`,
- SilenceUsage: true,
- }
- root.PersistentFlags().StringVar(&flagCountry, "country", "DE", "ISO country code for store region")
- root.PersistentFlags().StringVar(&flagLocale, "locale", "de", "store locale (e.g. de, en-US)")
- root.PersistentFlags().BoolVar(&flagJSON, "json", false, "output as JSON")
- root.AddCommand(listCmd(), watchCmd())
- if err := root.Execute(); err != nil {
- os.Exit(1)
- }
- }
- func newClient() *epic.Client {
- c := epic.NewClient()
- c.Country = flagCountry
- c.Locale = flagLocale
- return c
- }
- func printGames(cmd *cobra.Command, games []epic.Game) error {
- if flagJSON {
- enc := json.NewEncoder(os.Stdout)
- enc.SetIndent("", " ")
- return enc.Encode(games)
- }
- if len(games) == 0 {
- fmt.Fprintln(cmd.OutOrStdout(), "No free games on offer right now.")
- return nil
- }
- out := cmd.OutOrStdout()
- for _, g := range games {
- fmt.Fprintf(out, "🎮 %s\n", g.Title)
- if g.Seller != "" {
- fmt.Fprintf(out, " By: %s\n", g.Seller)
- }
- if g.PriceFmt != "" {
- fmt.Fprintf(out, " Instead of: %s\n", g.PriceFmt)
- }
- fmt.Fprintf(out, " Free until: %s\n", g.EndDate.Local().Format("02.01.2006 15:04 (MST)"))
- fmt.Fprintf(out, " %s\n", g.URL)
- fmt.Fprintln(out)
- }
- return nil
- }
- func listCmd() *cobra.Command {
- return &cobra.Command{
- Use: "list",
- Aliases: []string{"ls"},
- Short: "List games currently free",
- RunE: func(cmd *cobra.Command, args []string) error {
- games, err := newClient().GetFreeGames(cmd.Context())
- if err != nil {
- return err
- }
- return printGames(cmd, games)
- },
- }
- }
- func watchCmd() *cobra.Command {
- var (
- interval time.Duration
- once bool
- )
- cmd := &cobra.Command{
- Use: "watch",
- Short: "Wait for new free games (polling)",
- Long: `Polls the store and reports every game that becomes newly free.
- The first pass records the current state as the baseline (no reports).
- Exit with Ctrl+C.`,
- RunE: func(cmd *cobra.Command, args []string) error {
- ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM)
- defer stop()
- c := newClient()
- if once {
- games, err := c.GetFreeGames(ctx)
- if err != nil {
- return err
- }
- return printGames(cmd, games)
- }
- w := watch.NewWatcher(c, interval)
- return w.Run(ctx, false, watch.NotifierFunc(func(games []epic.Game) {
- fmt.Fprintf(os.Stderr, "\n🔔 New free games (%s):\n", time.Now().Format("15:04:05"))
- for _, g := range games {
- fmt.Fprintf(os.Stderr, " 🎮 %s — until %s — %s\n",
- g.Title, g.EndDate.Local().Format("02.01."), g.URL)
- }
- }))
- },
- }
- cmd.Flags().DurationVar(&interval, "interval", 15*time.Minute, "Polling interval")
- cmd.Flags().BoolVar(&once, "once", false, "Check once and exit")
- return cmd
- }
|