create UpdateChecker background task + constants refactoring

This commit is contained in:
Drew Weymouth
2023-03-30 19:19:00 -07:00
parent 2d7e633690
commit cca12d7327
6 changed files with 121 additions and 58 deletions
+28 -23
View File
@@ -10,15 +10,14 @@ import (
"supersonic/backend/util" "supersonic/backend/util"
"supersonic/player" "supersonic/player"
"supersonic/sharedutil" "supersonic/sharedutil"
"time"
"github.com/20after4/configdir" "github.com/20after4/configdir"
"github.com/zalando/go-keyring" "github.com/zalando/go-keyring"
) )
const ( const (
AppName = "supersonic" configFile = "config.toml"
configFile = "config.toml"
LatestReleaseURL = "https://github.com/dweymouth/supersonic/releases/latest"
) )
var ( var (
@@ -32,21 +31,27 @@ type App struct {
LibraryManager *LibraryManager LibraryManager *LibraryManager
PlaybackManager *PlaybackManager PlaybackManager *PlaybackManager
Player *player.Player Player *player.Player
UpdateChecker UpdateChecker
bgrndCtx context.Context appName string
cancel context.CancelFunc appVersionTag string
bgrndCtx context.Context
cancel context.CancelFunc
} }
func StartupApp() (*App, error) { func StartupApp(appName, appVersionTag, latestReleaseURL string) (*App, error) {
a := &App{} a := &App{appName: appName, appVersionTag: appVersionTag}
a.bgrndCtx, a.cancel = context.WithCancel(context.Background()) a.bgrndCtx, a.cancel = context.WithCancel(context.Background())
log.Printf("Starting %s...", AppName) log.Printf("Starting %s...", appName)
log.Printf("Using config dir: %s", configdir.LocalConfig(AppName)) log.Printf("Using config dir: %s", configdir.LocalConfig(appName))
log.Printf("Using cache dir: %s", configdir.LocalCache(AppName)) log.Printf("Using cache dir: %s", configdir.LocalCache(appName))
a.readConfig() a.readConfig()
a.UpdateChecker = NewUpdateChecker(appVersionTag, latestReleaseURL, &a.Config.Application.LastCheckedVersion)
a.UpdateChecker.Start(a.bgrndCtx, 24*time.Hour)
if err := a.initMPV(); err != nil { if err := a.initMPV(); err != nil {
return nil, err return nil, err
} }
@@ -63,10 +68,10 @@ func StartupApp() (*App, error) {
PreampGain: a.Config.ReplayGain.PreampGainDB, PreampGain: a.Config.ReplayGain.PreampGainDB,
}) })
a.ServerManager = NewServerManager() a.ServerManager = NewServerManager(appName)
a.PlaybackManager = NewPlaybackManager(a.bgrndCtx, a.ServerManager, a.Player, &a.Config.Scrobbling) a.PlaybackManager = NewPlaybackManager(a.bgrndCtx, a.ServerManager, a.Player, &a.Config.Scrobbling)
a.LibraryManager = NewLibraryManager(a.ServerManager) a.LibraryManager = NewLibraryManager(a.ServerManager)
a.ImageManager = NewImageManager(a.bgrndCtx, a.ServerManager, configdir.LocalCache(AppName)) a.ImageManager = NewImageManager(a.bgrndCtx, a.ServerManager, configdir.LocalCache(a.appName))
a.LibraryManager.PreCacheCoverFn = func(coverID string) { a.LibraryManager.PreCacheCoverFn = func(coverID string) {
_, _ = a.ImageManager.GetAlbumThumbnail(coverID) _, _ = a.ImageManager.GetAlbumThumbnail(coverID)
} }
@@ -75,23 +80,23 @@ func StartupApp() (*App, error) {
} }
func (a *App) readConfig() { func (a *App) readConfig() {
configdir.MakePath(configdir.LocalConfig(AppName)) configdir.MakePath(configdir.LocalConfig(a.appName))
cfgPath := configPath() cfgPath := a.configPath()
cfg, err := ReadConfigFile(cfgPath) cfg, err := ReadConfigFile(cfgPath, a.appVersionTag)
if err != nil { if err != nil {
log.Printf("Error reading app config file: %v", err) log.Printf("Error reading app config file: %v", err)
cfg = DefaultConfig() cfg = DefaultConfig(a.appVersionTag)
if _, err := os.Stat(cfgPath); err == nil { if _, err := os.Stat(cfgPath); err == nil {
backupCfgName := fmt.Sprintf("%s.bak", configFile) backupCfgName := fmt.Sprintf("%s.bak", configFile)
log.Printf("Config file may be malformed: copying to %s", backupCfgName) log.Printf("Config file may be malformed: copying to %s", backupCfgName)
_ = util.CopyFile(cfgPath, path.Join(configdir.LocalConfig(AppName), backupCfgName)) _ = util.CopyFile(cfgPath, path.Join(configdir.LocalConfig(a.appName), backupCfgName))
} }
} }
a.Config = cfg a.Config = cfg
} }
func (a *App) initMPV() error { func (a *App) initMPV() error {
p := player.NewWithClientName(AppName) p := player.NewWithClientName(a.appName)
c := a.Config.LocalPlayback c := a.Config.LocalPlayback
c.InMemoryCacheSizeMB = clamp(c.InMemoryCacheSizeMB, 10, 500) c.InMemoryCacheSizeMB = clamp(c.InMemoryCacheSizeMB, 10, 500)
if err := p.Init(c.AudioExclusive, c.InMemoryCacheSizeMB); err != nil { if err := p.Init(c.AudioExclusive, c.InMemoryCacheSizeMB); err != nil {
@@ -101,12 +106,12 @@ func (a *App) initMPV() error {
return nil return nil
} }
func (a *App) LoginToDefaultServer() error { func (a *App) LoginToDefaultServer(string) error {
serverCfg := a.Config.GetDefaultServer() serverCfg := a.Config.GetDefaultServer()
if serverCfg == nil { if serverCfg == nil {
return ErrNoServers return ErrNoServers
} }
pass, err := keyring.Get(AppName, serverCfg.ID.String()) pass, err := keyring.Get(a.appName, serverCfg.ID.String())
if err != nil { if err != nil {
return fmt.Errorf("error reading keyring credentials: %v", err) return fmt.Errorf("error reading keyring credentials: %v", err)
} }
@@ -118,11 +123,11 @@ func (a *App) Shutdown() {
a.Config.LocalPlayback.Volume = a.Player.GetVolume() a.Config.LocalPlayback.Volume = a.Player.GetVolume()
a.cancel() a.cancel()
a.Player.Destroy() a.Player.Destroy()
a.Config.WriteConfigFile(configPath()) a.Config.WriteConfigFile(a.configPath())
} }
func configPath() string { func (a *App) configPath() string {
return path.Join(configdir.LocalConfig(AppName), configFile) return path.Join(configdir.LocalConfig(a.appName), configFile)
} }
func clamp(i, min, max int) int { func clamp(i, min, max int) int {
+9 -7
View File
@@ -17,8 +17,9 @@ type ServerConfig struct {
} }
type AppConfig struct { type AppConfig struct {
WindowWidth int WindowWidth int
WindowHeight int WindowHeight int
LastCheckedVersion string
} }
type AlbumPageConfig struct { type AlbumPageConfig struct {
@@ -79,11 +80,12 @@ type Config struct {
ReplayGain ReplayGainConfig ReplayGain ReplayGainConfig
} }
func DefaultConfig() *Config { func DefaultConfig(appVersionTag string) *Config {
return &Config{ return &Config{
Application: AppConfig{ Application: AppConfig{
WindowWidth: 1000, WindowWidth: 1000,
WindowHeight: 800, WindowHeight: 800,
LastCheckedVersion: appVersionTag,
}, },
AlbumPage: AlbumPageConfig{ AlbumPage: AlbumPageConfig{
TracklistColumns: []string{"Artist", "Time", "Plays", "Favorite"}, TracklistColumns: []string{"Artist", "Time", "Plays", "Favorite"},
@@ -123,14 +125,14 @@ func DefaultConfig() *Config {
} }
} }
func ReadConfigFile(filepath string) (*Config, error) { func ReadConfigFile(filepath, appVersionTag string) (*Config, error) {
f, err := os.Open(filepath) f, err := os.Open(filepath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer f.Close() defer f.Close()
c := DefaultConfig() c := DefaultConfig(appVersionTag)
if err := toml.NewDecoder(f).Decode(c); err != nil { if err := toml.NewDecoder(f).Decode(c); err != nil {
return nil, err return nil, err
} }
+6 -5
View File
@@ -14,14 +14,15 @@ type ServerManager struct {
ServerID uuid.UUID ServerID uuid.UUID
Server *subsonic.Client Server *subsonic.Client
appName string
onServerConnected []func() onServerConnected []func()
onLogout []func() onLogout []func()
} }
var ErrUnreachable = errors.New("server is unreachable") var ErrUnreachable = errors.New("server is unreachable")
func NewServerManager() *ServerManager { func NewServerManager(appName string) *ServerManager {
return &ServerManager{} return &ServerManager{appName: appName}
} }
func (s *ServerManager) ConnectToServer(conf *ServerConfig, password string) error { func (s *ServerManager) ConnectToServer(conf *ServerConfig, password string) error {
@@ -75,7 +76,7 @@ func (s *ServerManager) testConnectionAndCreateClient(hostname, username, passwo
func (s *ServerManager) Logout() { func (s *ServerManager) Logout() {
if s.Server != nil { if s.Server != nil {
keyring.Delete(AppName, s.ServerID.String()) keyring.Delete(s.appName, s.ServerID.String())
for _, cb := range s.onLogout { for _, cb := range s.onLogout {
cb() cb()
} }
@@ -93,9 +94,9 @@ func (s *ServerManager) OnLogout(cb func()) {
} }
func (s *ServerManager) GetServerPassword(server *ServerConfig) (string, error) { func (s *ServerManager) GetServerPassword(server *ServerConfig) (string, error) {
return keyring.Get(AppName, server.ID.String()) return keyring.Get(s.appName, server.ID.String())
} }
func (s *ServerManager) SetServerPassword(server *ServerConfig, password string) error { func (s *ServerManager) SetServerPassword(server *ServerConfig, password string) error {
return keyring.Set(AppName, server.ID.String(), password) return keyring.Set(s.appName, server.ID.String(), password)
} }
+71
View File
@@ -0,0 +1,71 @@
package backend
import (
"context"
"log"
"net/http"
"strings"
"time"
)
type UpdateChecker struct {
OnUpdatedVersionFound func(releaseURL string)
foundUpdate bool
latestReleaseURL string
appVersionTag string
lastCheckedTag *string
}
func NewUpdateChecker(appVersionTag, latestReleaseURL string, lastCheckedTag *string) UpdateChecker {
return UpdateChecker{
appVersionTag: appVersionTag,
latestReleaseURL: latestReleaseURL,
lastCheckedTag: lastCheckedTag,
}
}
func (u *UpdateChecker) Start(ctx context.Context, interval time.Duration) {
go func() {
u.checkForUpdate() // check once at startup
t := time.NewTicker(interval)
for {
select {
case <-ctx.Done():
return
case <-t.C:
u.checkForUpdate()
}
}
}()
}
func (u *UpdateChecker) UpdateAvailable() bool {
return u.foundUpdate
}
func (u *UpdateChecker) checkForUpdate() {
t := u.latestVersionTag()
if t != "" && t != *u.lastCheckedTag {
u.foundUpdate = true
*u.lastCheckedTag = t
if u.OnUpdatedVersionFound != nil {
u.OnUpdatedVersionFound(u.latestReleaseURL)
}
}
}
func (u *UpdateChecker) latestVersionTag() string {
resp, err := http.Head(u.latestReleaseURL)
if err != nil {
log.Printf("failed to check for newest version: %s", err.Error())
return ""
}
url := resp.Request.URL.String()
url = strings.TrimSuffix(url, "/")
idx := strings.LastIndex(url, "/")
if idx >= len(url)-1 {
return ""
}
return url[idx+1:]
}
-18
View File
@@ -2,11 +2,8 @@ package util
import ( import (
"io" "io"
"log"
"math/rand" "math/rand"
"net/http"
"os" "os"
"strings"
"time" "time"
) )
@@ -40,18 +37,3 @@ func CopyFile(srcPath, dstPath string) error {
_, err = io.Copy(fout, fin) _, err = io.Copy(fout, fin)
return err return err
} }
func LatestVersionTag(latestReleaseURL string) string {
resp, err := http.Head(latestReleaseURL)
if err != nil {
log.Printf("failed to check for newest version: %s", err.Error())
return ""
}
url := resp.Request.URL.String()
url = strings.TrimSuffix(url, "/")
idx := strings.LastIndex(url, "/")
if idx >= len(url)-1 {
return ""
}
return url[idx+1:]
}
+7 -5
View File
@@ -14,10 +14,12 @@ import (
) )
const ( const (
appname = "supersonic" appname = "supersonic"
displayName = "Supersonic" displayName = "Supersonic"
appVersion = "0.0.1-alpha2" appVersion = "0.0.1-alpha2"
configFile = "config.toml" appVersionTag = "v" + appVersion
configFile = "config.toml"
latestReleaseURL = "https://github.com/dweymouth/supersonic/releases/latest"
) )
func configPath() string { func configPath() string {
@@ -25,7 +27,7 @@ func configPath() string {
} }
func main() { func main() {
myApp, err := backend.StartupApp() myApp, err := backend.StartupApp(appname, appVersionTag, latestReleaseURL)
if err != nil { if err != nil {
log.Fatalf("fatal startup error: %v", err.Error()) log.Fatalf("fatal startup error: %v", err.Error())
} }