diff --git a/backend/app.go b/backend/app.go index eecc85f..93c4d9d 100644 --- a/backend/app.go +++ b/backend/app.go @@ -81,17 +81,6 @@ func StartupApp(appName, displayAppName, appVersionTag, latestReleaseURL string) configdir.MakePath(confDir) 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{ appName: appName, appVersionTag: appVersionTag, @@ -101,7 +90,20 @@ func StartupApp(appName, displayAppName, appVersionTag, latestReleaseURL string) } a.bgrndCtx, a.cancel = context.WithCancel(context.Background()) 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.Start(a.bgrndCtx, 24*time.Hour) @@ -137,6 +139,8 @@ func StartupApp(appName, displayAppName, appVersionTag, latestReleaseURL string) return a.ImageManager.GetCoverArtUrl(id) }) + a.startConfigWriter(a.bgrndCtx) + return a, nil } @@ -367,6 +371,34 @@ func (a *App) SaveConfigFile() { 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 { return path.Join(a.configDir, configFile) } diff --git a/backend/cmdlineoptions.go b/backend/cmdlineoptions.go new file mode 100644 index 0000000..5e189ed --- /dev/null +++ b/backend/cmdlineoptions.go @@ -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 - )", 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 +} diff --git a/backend/ipc/api.go b/backend/ipc/api.go index be82574..fbea445 100644 --- a/backend/ipc/api.go +++ b/backend/ipc/api.go @@ -1,43 +1,29 @@ package ipc +import "fmt" + const ( - // GET - PingPath = "/ping" - - // POST - PlayPath = "/transport/play" - // POST + PingPath = "/ping" + PlayPath = "/transport/play" PlayPausePath = "/transport/playpause" - // POST - PausePath = "/transport/pause" - // POST - StopPath = "/transport/stop" - // POST - PreviousPath = "/transport/previous" - // POST - NextPath = "/transport/next" - // POST(TimePos) - TimePosPath = "/transport/timepos" - // POST to seek - PlayTrackPath = "/queue/playtrack" - // GET -> Volume - // POST(Volume) - VolumePath = "/volume" - - // POST - ShowPath = "/window/show" - // POST - QuitPath = "/window/quit" + PausePath = "/transport/pause" + StopPath = "/transport/stop" + PreviousPath = "/transport/previous" + NextPath = "/transport/next" + TimePosPath = "/transport/timepos" // ?s= + VolumePath = "/volume" // ?v= + ShowPath = "/window/show" + QuitPath = "/window/quit" ) -type TimePos struct { - Seconds float64 `json:"seconds"` -} - -type Volume struct { - Volume int `json:"volume"` -} - type Response struct { 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) +} diff --git a/backend/ipc/client.go b/backend/ipc/client.go index 34276bb..3b31ff1 100644 --- a/backend/ipc/client.go +++ b/backend/ipc/client.go @@ -25,56 +25,56 @@ func Connect() (*Client, error) { }, }} if err := client.Ping(); err != nil { - log.Println("ping error") return nil, err } return client, nil } func (c *Client) Ping() error { - if c.makeSimpleRequest(http.MethodGet, PingPath) != nil { + if c.sendRequest(PingPath) != nil { return ErrPingFail } return nil } func (c *Client) Play() error { - return c.makeSimpleRequest(http.MethodPost, PlayPath) + return c.sendRequest(PlayPath) } func (c *Client) Pause() error { - return c.makeSimpleRequest(http.MethodPost, PausePath) + return c.sendRequest(PausePath) } func (c *Client) PlayPause() error { - return c.makeSimpleRequest(http.MethodPost, PlayPausePath) + return c.sendRequest(PlayPausePath) } func (c *Client) SeekNext() error { - return c.makeSimpleRequest(http.MethodPost, NextPath) + return c.sendRequest(NextPath) } 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 { - return c.makeSimpleRequest(http.MethodPost, ShowPath) + return c.sendRequest(ShowPath) } func (c *Client) Quit() error { - return c.makeSimpleRequest(http.MethodPost, QuitPath) + return c.sendRequest(QuitPath) } -func (c *Client) makeSimpleRequest(method string, path string) error { - var resp *http.Response - 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) - } +func (c *Client) sendRequest(path string) error { + resp, err := c.httpC.Get("http://supersonic/" + path) if err != nil { log.Printf("http err: %v\n", err) diff --git a/backend/ipc/conn_other.go b/backend/ipc/conn_other.go index 0dd4e90..9fd6ca7 100644 --- a/backend/ipc/conn_other.go +++ b/backend/ipc/conn_other.go @@ -3,19 +3,41 @@ package ipc import ( + "fmt" "net" "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) { // 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) { - return net.Listen("unix", "/tmp/supersonic.sock") + return net.Listen("unix", socketPath) } func DestroyConn() error { - return os.Remove("/tmp/supersonic.sock") + return os.Remove(socketPath) } diff --git a/backend/ipc/server.go b/backend/ipc/server.go index 960c20e..fc62ed3 100644 --- a/backend/ipc/server.go +++ b/backend/ipc/server.go @@ -5,6 +5,7 @@ import ( "encoding/json" "net" "net/http" + "strconv" ) type PlaybackHandler interface { @@ -71,25 +72,20 @@ func (s *serverImpl) createHandler() http.Handler { m.HandleFunc(PreviousPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekBackOrPrevious)) m.HandleFunc(NextPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekNext)) m.HandleFunc(TimePosPath, func(w http.ResponseWriter, r *http.Request) { - var t TimePos - if err := json.NewDecoder(r.Response.Body).Decode(&t); err != nil { + _s := r.URL.Query().Get("s") + if secs, err := strconv.ParseFloat(_s, 64); err == nil { + s.writeSimpleResponse(w, s.pbHandler.SeekSeconds(secs)) + } else { s.writeErr(w, err) - return } - s.writeSimpleResponse(w, s.pbHandler.SeekSeconds(t.Seconds)) }) m.HandleFunc(VolumePath, func(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodGet { - msg, _ := json.Marshal(Volume{Volume: s.pbHandler.Volume()}) - w.Write(msg) - return - } - var v Volume - if err := json.NewDecoder(r.Response.Body).Decode(&v); err != nil { + v := r.URL.Query().Get("v") + if vol, err := strconv.Atoi(v); err == nil { + s.writeSimpleResponse(w, s.pbHandler.SetVolume(vol)) + } else { s.writeErr(w, err) - return } - s.writeSimpleResponse(w, s.pbHandler.SetVolume(v.Volume)) }) return m } diff --git a/main.go b/main.go index 212d0ae..513bef5 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,8 @@ package main import ( + "flag" + "fmt" "log" "os" "runtime" @@ -10,14 +12,28 @@ import ( "github.com/dweymouth/supersonic/res" "github.com/dweymouth/supersonic/ui" - "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" ) 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) if err != nil { - log.Fatalf("fatal startup error: %v", err.Error()) + if err != backend.ErrAnotherInstance { + log.Fatalf("fatal startup error: %v", err.Error()) + } + return } if myApp.Config.Application.UIScaleSize == "Smaller" { @@ -29,15 +45,7 @@ func main() { fyneApp := app.New() fyneApp.SetIcon(res.ResAppicon256Png) - w := float32(myApp.Config.Application.WindowWidth) - 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)) + mainWindow := ui.NewMainWindow(fyneApp, res.AppName, res.DisplayName, res.AppVersion, myApp) myApp.OnReactivate = mainWindow.Show myApp.OnExit = mainWindow.Quit diff --git a/ui/mainwindow.go b/ui/mainwindow.go index 1c24141..10b2111 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -60,7 +60,7 @@ type MainWindow struct { 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{ App: app, 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.container = container.NewBorder(nil, m.BottomPanel, nil, nil, m.BrowsingPane) 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) { if item == nil { m.Window.SetTitle(displayAppName)