diff --git a/main.go b/main.go index bb60c41..0e8593c 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "go.uber.org/fx" "git.asdf.cafe/abs3nt/gospt-ng/src/app" + "git.asdf.cafe/abs3nt/gospt-ng/src/components/cache" "git.asdf.cafe/abs3nt/gospt-ng/src/components/cli" "git.asdf.cafe/abs3nt/gospt-ng/src/components/commands" ) @@ -14,6 +15,7 @@ func main() { fx.Populate(&s), app.Config, fx.Provide( + cache.NewCache, commands.NewCommander, ), fx.Invoke( diff --git a/src/components/cache/cache.go b/src/components/cache/cache.go new file mode 100644 index 0000000..ae898fa --- /dev/null +++ b/src/components/cache/cache.go @@ -0,0 +1,117 @@ +package cache + +import ( + "encoding/json" + "fmt" + "log/slog" + "os" + "path/filepath" + "time" + + "go.uber.org/fx" +) + +type CacheEntry struct { + Expire time.Time `json:"e"` + Value string `json:"v"` +} + +type CacheResult struct { + fx.Out + + Cache *Cache +} + +type Cache struct { + Root string + Log *slog.Logger +} + +type CacheParams struct { + fx.In + + Log *slog.Logger +} + +func NewCache(p CacheParams) CacheResult { + c := &Cache{ + Root: filepath.Join(os.TempDir(), "gospt.cache"), + Log: p.Log, + } + return CacheResult{ + Cache: c, + } +} + +func (c *Cache) load() (map[string]CacheEntry, error) { + out := map[string]CacheEntry{} + cache, err := os.Open(c.Root) + if err != nil { + return nil, err + } + if err := json.NewDecoder(cache).Decode(&out); err != nil { + return nil, err + } + return out, nil +} + +func (c *Cache) save(m map[string]CacheEntry) error { + payload, err := json.Marshal(m) + if err != nil { + return err + } + slog.Debug("CACHE", "saving", string(payload)) + err = os.WriteFile(c.Root, payload, 0o600) + if err != nil { + return err + } + return nil +} + +func (c *Cache) GetOrDo(key string, do func() (string, error), ttl time.Duration) (string, error) { + conf, err := c.load() + if err != nil { + slog.Debug("CACHE", "failed read", err) + return c.Do(key, do, ttl) + } + val, ok := conf[key] + if !ok { + return c.Do(key, do, ttl) + } + if time.Now().After(val.Expire) { + return c.Do(key, do, ttl) + } + return val.Value, nil +} + +func (c *Cache) Do(key string, do func() (string, error), ttl time.Duration) (string, error) { + if do == nil { + return "", nil + } + res, err := do() + if err != nil { + return "", err + } + return c.Put(key, res, ttl) +} + +func (c *Cache) Put(key string, value string, ttl time.Duration) (string, error) { + conf, err := c.load() + if err != nil { + conf = map[string]CacheEntry{} + } + conf[key] = CacheEntry{ + Expire: time.Now().Add(ttl), + Value: value, + } + slog.Debug("CACHE", "new item", fmt.Sprintf("%s: %s", key, value)) + err = c.save(conf) + if err != nil { + slog.Debug("CACHE", "failed to save", err) + } + return value, nil +} + +func (c *Cache) Clear() error { + return os.Remove(c.Root) +} diff --git a/src/components/cli/cli.go b/src/components/cli/cli.go index eed1c5e..c513b02 100644 --- a/src/components/cli/cli.go +++ b/src/components/cli/cli.go @@ -204,6 +204,13 @@ func Run(c *commands.Commander, s fx.Shutdowner) { return c.ClearRadio() }, }, + { + Name: "status", + Usage: "Prints the current status", + Action: func(ctx *cli.Context) error { + return c.Status() + }, + }, { Name: "devices", Usage: "Lists available devices", diff --git a/src/components/commands/commander.go b/src/components/commands/commander.go index 54785bb..32cd8fb 100644 --- a/src/components/commands/commander.go +++ b/src/components/commands/commander.go @@ -7,6 +7,8 @@ import ( "github.com/zmb3/spotify/v2" "go.uber.org/fx" + + "git.asdf.cafe/abs3nt/gospt-ng/src/components/cache" ) type CommanderResult struct { @@ -21,6 +23,7 @@ type CommanderParams struct { Context context.Context Client *spotify.Client Log *slog.Logger + Cache *cache.Cache } type Commander struct { @@ -28,6 +31,7 @@ type Commander struct { Client *spotify.Client User *spotify.PrivateUser Log *slog.Logger + Cache *cache.Cache } func NewCommander(p CommanderParams) CommanderResult { @@ -41,6 +45,7 @@ func NewCommander(p CommanderParams) CommanderResult { Client: p.Client, User: currentUser, Log: p.Log, + Cache: p.Cache, } return CommanderResult{ Commander: c, diff --git a/src/components/commands/status.go b/src/components/commands/status.go new file mode 100644 index 0000000..de847bd --- /dev/null +++ b/src/components/commands/status.go @@ -0,0 +1,38 @@ +package commands + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/zmb3/spotify/v2" +) + +func (c *Commander) Status() error { + state, err := c.Cache.GetOrDo("state", func() (string, error) { + state, err := c.Client.PlayerState(c.Context) + if err != nil { + return "", err + } + str, err := c.FormatState(state) + if err != nil { + return "", nil + } + return str, nil + }, 5*time.Second) + if err != nil { + return err + } + fmt.Println(state) + return nil +} + +func (c *Commander) FormatState(state *spotify.PlayerState) (string, error) { + state.Item.AvailableMarkets = []string{} + state.Item.Album.AvailableMarkets = []string{} + out, err := json.MarshalIndent(state, "", " ") + if err != nil { + return "", err + } + return (string(out)), nil +}