add cmdline flags and some fixes to the IPC layer

This commit is contained in:
Drew Weymouth
2024-06-12 17:21:15 -07:00
parent 8e815c644a
commit 280030c858
8 changed files with 187 additions and 93 deletions
+44 -12
View File
@@ -81,17 +81,6 @@ func StartupApp(appName, displayAppName, appVersionTag, latestReleaseURL string)
configdir.MakePath(confDir) configdir.MakePath(confDir)
configdir.MakePath(cacheDir) configdir.MakePath(cacheDir)
cli, err := ipc.Connect()
if err == nil {
log.Println("Another instance is running. Reactivating it...")
cli.Show()
return nil, ErrAnotherInstance
}
log.Printf("Starting %s...", appName)
log.Printf("Using config dir: %s", confDir)
log.Printf("Using cache dir: %s", cacheDir)
a := &App{ a := &App{
appName: appName, appName: appName,
appVersionTag: appVersionTag, appVersionTag: appVersionTag,
@@ -101,7 +90,20 @@ func StartupApp(appName, displayAppName, appVersionTag, latestReleaseURL string)
} }
a.bgrndCtx, a.cancel = context.WithCancel(context.Background()) a.bgrndCtx, a.cancel = context.WithCancel(context.Background())
a.readConfig() a.readConfig()
a.startConfigWriter(a.bgrndCtx)
if HaveCommandLineOptions() || !a.Config.Application.AllowMultiInstance {
connected, err := a.checkCLIFlagsAndSendIPCMsg()
if err != nil {
log.Printf("error sending IPC message: %s", err.Error())
}
if connected /* we reached the other instance at all */ {
return nil, ErrAnotherInstance
}
}
log.Printf("Starting %s...", appName)
log.Printf("Using config dir: %s", confDir)
log.Printf("Using cache dir: %s", cacheDir)
a.UpdateChecker = NewUpdateChecker(appVersionTag, latestReleaseURL, &a.Config.Application.LastCheckedVersion) a.UpdateChecker = NewUpdateChecker(appVersionTag, latestReleaseURL, &a.Config.Application.LastCheckedVersion)
a.UpdateChecker.Start(a.bgrndCtx, 24*time.Hour) a.UpdateChecker.Start(a.bgrndCtx, 24*time.Hour)
@@ -137,6 +139,8 @@ func StartupApp(appName, displayAppName, appVersionTag, latestReleaseURL string)
return a.ImageManager.GetCoverArtUrl(id) return a.ImageManager.GetCoverArtUrl(id)
}) })
a.startConfigWriter(a.bgrndCtx)
return a, nil return a, nil
} }
@@ -367,6 +371,34 @@ func (a *App) SaveConfigFile() {
a.lastWrittenCfg = *a.Config a.lastWrittenCfg = *a.Config
} }
func (a *App) checkCLIFlagsAndSendIPCMsg() (connected bool, err error) {
cli, err := ipc.Connect()
if err != nil {
return false, err
}
switch {
case *FlagPlay:
err = cli.Play()
case *FlagPause:
err = cli.Pause()
case *FlagPlayPause:
err = cli.PlayPause()
case *FlagPrevious:
err = cli.SeekBackOrPrevious()
case *FlagNext:
err = cli.SeekNext()
case VolumeCLIArg >= 0:
err = cli.SetVolume(VolumeCLIArg)
case SeekToCLIArg >= 0:
err = cli.SeekSeconds(SeekToCLIArg)
default:
log.Println("Another instance is running. Reactivating it...")
err = cli.Show()
}
return true, err
}
func (a *App) configFilePath() string { func (a *App) configFilePath() string {
return path.Join(a.configDir, configFile) return path.Join(a.configDir, configFile)
} }
+41
View File
@@ -0,0 +1,41 @@
package backend
import (
"flag"
"strconv"
)
var (
VolumeCLIArg int = -1
SeekToCLIArg float64 = -1
FlagPlay = flag.Bool("play", false, "unpause or begin playback")
FlagPause = flag.Bool("pause", false, "pause playback")
FlagPlayPause = flag.Bool("play-pause", false, "toggle play/pause state")
FlagPrevious = flag.Bool("previous", false, "seek to previous track or beginning of current")
FlagNext = flag.Bool("next", false, "seek to next track")
FlagVersion = flag.Bool("version", false, "print app version and exit")
FlagHelp = flag.Bool("help", false, "print command line options and exit")
)
func init() {
flag.Func("volume", "sets the playback volume (0-100)", func(s string) error {
v, err := strconv.Atoi(s)
VolumeCLIArg = v
return err
})
flag.Func("seek-to", "seeks to the given position in seconds in the current file (0.0 - <trackDur>)", func(s string) error {
v, err := strconv.ParseFloat(s, 64)
SeekToCLIArg = v
return err
})
}
func HaveCommandLineOptions() bool {
visitedAny := false
flag.Visit(func(*flag.Flag) {
visitedAny = true
})
return visitedAny
}
+12 -26
View File
@@ -1,43 +1,29 @@
package ipc package ipc
import "fmt"
const ( const (
// GET
PingPath = "/ping" PingPath = "/ping"
// POST
PlayPath = "/transport/play" PlayPath = "/transport/play"
// POST
PlayPausePath = "/transport/playpause" PlayPausePath = "/transport/playpause"
// POST
PausePath = "/transport/pause" PausePath = "/transport/pause"
// POST
StopPath = "/transport/stop" StopPath = "/transport/stop"
// POST
PreviousPath = "/transport/previous" PreviousPath = "/transport/previous"
// POST
NextPath = "/transport/next" NextPath = "/transport/next"
// POST(TimePos) TimePosPath = "/transport/timepos" // ?s=<seconds>
TimePosPath = "/transport/timepos" VolumePath = "/volume" // ?v=<vol>
// POST to seek
PlayTrackPath = "/queue/playtrack"
// GET -> Volume
// POST(Volume)
VolumePath = "/volume"
// POST
ShowPath = "/window/show" ShowPath = "/window/show"
// POST
QuitPath = "/window/quit" QuitPath = "/window/quit"
) )
type TimePos struct {
Seconds float64 `json:"seconds"`
}
type Volume struct {
Volume int `json:"volume"`
}
type Response struct { type Response struct {
Error string `json:"error"` Error string `json:"error"`
} }
func SetVolumePath(vol int) string {
return fmt.Sprintf("%s?v=%d", VolumePath, vol)
}
func SeekToSecondsPath(secs float64) string {
return fmt.Sprintf("%s?s=%0.2f", TimePosPath, secs)
}
+18 -18
View File
@@ -25,56 +25,56 @@ func Connect() (*Client, error) {
}, },
}} }}
if err := client.Ping(); err != nil { if err := client.Ping(); err != nil {
log.Println("ping error")
return nil, err return nil, err
} }
return client, nil return client, nil
} }
func (c *Client) Ping() error { func (c *Client) Ping() error {
if c.makeSimpleRequest(http.MethodGet, PingPath) != nil { if c.sendRequest(PingPath) != nil {
return ErrPingFail return ErrPingFail
} }
return nil return nil
} }
func (c *Client) Play() error { func (c *Client) Play() error {
return c.makeSimpleRequest(http.MethodPost, PlayPath) return c.sendRequest(PlayPath)
} }
func (c *Client) Pause() error { func (c *Client) Pause() error {
return c.makeSimpleRequest(http.MethodPost, PausePath) return c.sendRequest(PausePath)
} }
func (c *Client) PlayPause() error { func (c *Client) PlayPause() error {
return c.makeSimpleRequest(http.MethodPost, PlayPausePath) return c.sendRequest(PlayPausePath)
} }
func (c *Client) SeekNext() error { func (c *Client) SeekNext() error {
return c.makeSimpleRequest(http.MethodPost, NextPath) return c.sendRequest(NextPath)
} }
func (c *Client) SeekBackOrPrevious() error { func (c *Client) SeekBackOrPrevious() error {
return c.makeSimpleRequest(http.MethodPost, NextPath) return c.sendRequest(PreviousPath)
}
func (c *Client) SeekSeconds(secs float64) error {
return c.sendRequest(SeekToSecondsPath(secs))
}
func (c *Client) SetVolume(vol int) error {
return c.sendRequest(SetVolumePath(vol))
} }
func (c *Client) Show() error { func (c *Client) Show() error {
return c.makeSimpleRequest(http.MethodPost, ShowPath) return c.sendRequest(ShowPath)
} }
func (c *Client) Quit() error { func (c *Client) Quit() error {
return c.makeSimpleRequest(http.MethodPost, QuitPath) return c.sendRequest(QuitPath)
} }
func (c *Client) makeSimpleRequest(method string, path string) error { func (c *Client) sendRequest(path string) error {
var resp *http.Response resp, err := c.httpC.Get("http://supersonic/" + path)
var err error
switch method {
case http.MethodGet:
resp, err = c.httpC.Get("http://supersonic/" + path)
case http.MethodPost:
resp, err = c.httpC.Post("http://supersonic/"+path, "application/json", nil)
}
if err != nil { if err != nil {
log.Printf("http err: %v\n", err) log.Printf("http err: %v\n", err)
+25 -3
View File
@@ -3,19 +3,41 @@
package ipc package ipc
import ( import (
"fmt"
"net" "net"
"os" "os"
"os/user"
"path"
"runtime"
) )
var socketPath = "/tmp/supersonic.sock"
func init() {
if runtime.GOOS == "darwin" {
if home, err := os.UserHomeDir(); err == nil {
socketPath = path.Join(home, "Library", "Caches", "supersonic", "supersonic.sock")
} else if user, err := user.Current(); err == nil {
socketPath = fmt.Sprintf("/tmp/supersonic-%s.sock", user.Name)
}
} else {
if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" {
socketPath = path.Join(runtime, "supersonic.sock")
} else if user, err := user.Current(); err == nil {
socketPath = fmt.Sprintf("/tmp/supersonic-%s.sock", user.Name)
}
}
}
func Dial() (net.Conn, error) { func Dial() (net.Conn, error) {
// TODO - use XDG runtime dir, also handle portable mode // TODO - use XDG runtime dir, also handle portable mode
return net.Dial("unix", "/tmp/supersonic.sock") return net.Dial("unix", socketPath)
} }
func Listen() (net.Listener, error) { func Listen() (net.Listener, error) {
return net.Listen("unix", "/tmp/supersonic.sock") return net.Listen("unix", socketPath)
} }
func DestroyConn() error { func DestroyConn() error {
return os.Remove("/tmp/supersonic.sock") return os.Remove(socketPath)
} }
+9 -13
View File
@@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"net" "net"
"net/http" "net/http"
"strconv"
) )
type PlaybackHandler interface { type PlaybackHandler interface {
@@ -71,25 +72,20 @@ func (s *serverImpl) createHandler() http.Handler {
m.HandleFunc(PreviousPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekBackOrPrevious)) m.HandleFunc(PreviousPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekBackOrPrevious))
m.HandleFunc(NextPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekNext)) m.HandleFunc(NextPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekNext))
m.HandleFunc(TimePosPath, func(w http.ResponseWriter, r *http.Request) { m.HandleFunc(TimePosPath, func(w http.ResponseWriter, r *http.Request) {
var t TimePos _s := r.URL.Query().Get("s")
if err := json.NewDecoder(r.Response.Body).Decode(&t); err != nil { if secs, err := strconv.ParseFloat(_s, 64); err == nil {
s.writeSimpleResponse(w, s.pbHandler.SeekSeconds(secs))
} else {
s.writeErr(w, err) s.writeErr(w, err)
return
} }
s.writeSimpleResponse(w, s.pbHandler.SeekSeconds(t.Seconds))
}) })
m.HandleFunc(VolumePath, func(w http.ResponseWriter, r *http.Request) { m.HandleFunc(VolumePath, func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet { v := r.URL.Query().Get("v")
msg, _ := json.Marshal(Volume{Volume: s.pbHandler.Volume()}) if vol, err := strconv.Atoi(v); err == nil {
w.Write(msg) s.writeSimpleResponse(w, s.pbHandler.SetVolume(vol))
return } else {
}
var v Volume
if err := json.NewDecoder(r.Response.Body).Decode(&v); err != nil {
s.writeErr(w, err) s.writeErr(w, err)
return
} }
s.writeSimpleResponse(w, s.pbHandler.SetVolume(v.Volume))
}) })
return m return m
} }
+18 -10
View File
@@ -1,6 +1,8 @@
package main package main
import ( import (
"flag"
"fmt"
"log" "log"
"os" "os"
"runtime" "runtime"
@@ -10,15 +12,29 @@ import (
"github.com/dweymouth/supersonic/res" "github.com/dweymouth/supersonic/res"
"github.com/dweymouth/supersonic/ui" "github.com/dweymouth/supersonic/ui"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app" "fyne.io/fyne/v2/app"
) )
func main() { func main() {
// parse cmd line flags - see backend/cmdlineoptions.go
flag.Parse()
if *backend.FlagVersion {
fmt.Println(res.AppVersion)
return
}
if *backend.FlagHelp {
flag.Usage()
return
}
// rest of flag actions are handled in backend.StartupApp
myApp, err := backend.StartupApp(res.AppName, res.DisplayName, res.AppVersionTag, res.LatestReleaseURL) myApp, err := backend.StartupApp(res.AppName, res.DisplayName, res.AppVersionTag, res.LatestReleaseURL)
if err != nil { if err != nil {
if err != backend.ErrAnotherInstance {
log.Fatalf("fatal startup error: %v", err.Error()) log.Fatalf("fatal startup error: %v", err.Error())
} }
return
}
if myApp.Config.Application.UIScaleSize == "Smaller" { if myApp.Config.Application.UIScaleSize == "Smaller" {
os.Setenv("FYNE_SCALE", "0.85") os.Setenv("FYNE_SCALE", "0.85")
@@ -29,15 +45,7 @@ func main() {
fyneApp := app.New() fyneApp := app.New()
fyneApp.SetIcon(res.ResAppicon256Png) fyneApp.SetIcon(res.ResAppicon256Png)
w := float32(myApp.Config.Application.WindowWidth) mainWindow := ui.NewMainWindow(fyneApp, res.AppName, res.DisplayName, res.AppVersion, myApp)
if w <= 1 {
w = 1000
}
h := float32(myApp.Config.Application.WindowHeight)
if h <= 1 {
h = 800
}
mainWindow := ui.NewMainWindow(fyneApp, res.AppName, res.DisplayName, res.AppVersion, myApp, fyne.NewSize(w, h))
myApp.OnReactivate = mainWindow.Show myApp.OnReactivate = mainWindow.Show
myApp.OnExit = mainWindow.Quit myApp.OnExit = mainWindow.Quit
+11 -2
View File
@@ -60,7 +60,7 @@ type MainWindow struct {
radioBtn *widget.Button radioBtn *widget.Button
} }
func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string, app *backend.App, size fyne.Size) MainWindow { func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string, app *backend.App) MainWindow {
m := MainWindow{ m := MainWindow{
App: app, App: app,
Window: fyneApp.NewWindow(displayAppName), Window: fyneApp.NewWindow(displayAppName),
@@ -89,7 +89,16 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string,
m.BottomPanel = NewBottomPanel(app.PlaybackManager, app.ImageManager, m.Controller) m.BottomPanel = NewBottomPanel(app.PlaybackManager, app.ImageManager, m.Controller)
m.container = container.NewBorder(nil, m.BottomPanel, nil, nil, m.BrowsingPane) m.container = container.NewBorder(nil, m.BottomPanel, nil, nil, m.BrowsingPane)
m.Window.SetContent(m.container) m.Window.SetContent(m.container)
m.Window.Resize(size)
w := float32(app.Config.Application.WindowWidth)
if w <= 1 {
w = 1000
}
h := float32(app.Config.Application.WindowHeight)
if h <= 1 {
h = 800
}
m.Window.Resize(fyne.NewSize(w, h))
app.PlaybackManager.OnSongChange(func(item mediaprovider.MediaItem, _ *mediaprovider.Track) { app.PlaybackManager.OnSongChange(func(item mediaprovider.MediaItem, _ *mediaprovider.Track) {
if item == nil { if item == nil {
m.Window.SetTitle(displayAppName) m.Window.SetTitle(displayAppName)