diff --git a/CHANGELOG.md b/CHANGELOG.md index a731971..3e2dfc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - [#117](https://github.com/dweymouth/supersonic/issues/117) Add (optional) system tray menu and close to tray support - [#55](https://github.com/dweymouth/supersonic/issues/55) Show disc number and disc count for multi-disc albums - [#115](https://github.com/dweymouth/supersonic/issues/115) Add search bar to artist, genres, and playlists pages +- [#104](https://github.com/dweymouth/supersonic/issues/104) Add alternate (e.g. external) hostname to server connection config ### Fixes - **todo-commithash** Don't show update available prompt if the found version is the same as the running app version diff --git a/README.md b/README.md index d8f072a..00f0f74 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,11 @@ Screenshots of Supersonic running against the Navidrome [demo server](https://ww ## Features -* [x] Fast, lightweight, native UI +* [x] Fast, lightweight, native UI with infinite scrolling * [x] High-quality gapless audio playback powered by MPV, with optional audio exclusive mode * [x] ReplayGain support (depends on files being tagged on server) -* [x] Infinite scrolling * [x] Scrobble plays to server, with configurable criteria +* [x] Primary and alternate server hostnames, e.g. for internal and external URLs * [x] Browse by albums, artists, genres, playlists * [x] Album and playlist views with tracklist and cover image * [x] Artist view with biography, image, similar artists, and discography diff --git a/backend/config.go b/backend/config.go index 413acb3..727e601 100644 --- a/backend/config.go +++ b/backend/config.go @@ -7,13 +7,18 @@ import ( "github.com/pelletier/go-toml" ) +type ServerConnection struct { + Hostname string + AltHostname string + Username string + LegacyAuth bool +} + type ServerConfig struct { - ID uuid.UUID - Nickname string - Hostname string - Username string - LegacyAuth bool - Default bool + ServerConnection + ID uuid.UUID + Nickname string + Default bool } type AppConfig struct { @@ -172,13 +177,11 @@ func (c *Config) SetDefaultServer(serverID uuid.UUID) { } } -func (c *Config) AddServer(nickname, hostname, username string, legacyAuth bool) *ServerConfig { +func (c *Config) AddServer(nickname string, connection ServerConnection) *ServerConfig { s := &ServerConfig{ - ID: uuid.New(), - Nickname: nickname, - Hostname: hostname, - Username: username, - LegacyAuth: legacyAuth, + ID: uuid.New(), + Nickname: nickname, + ServerConnection: connection, } c.Servers = append(c.Servers, s) return s diff --git a/backend/servermanager.go b/backend/servermanager.go index 38e372c..8df6ca1 100644 --- a/backend/servermanager.go +++ b/backend/servermanager.go @@ -1,6 +1,7 @@ package backend import ( + "context" "errors" "net/http" "time" @@ -26,7 +27,7 @@ func NewServerManager(appName string) *ServerManager { } func (s *ServerManager) ConnectToServer(conf *ServerConfig, password string) error { - cli, err := s.testConnectionAndCreateClient(conf.Hostname, conf.Username, password, conf.LegacyAuth) + cli, err := s.testConnectionAndCreateClient(conf.ServerConnection, password) if err != nil { return err } @@ -39,12 +40,12 @@ func (s *ServerManager) ConnectToServer(conf *ServerConfig, password string) err } func (s *ServerManager) TestConnectionAndAuth( - hostname, username, password string, legacyAuth bool, timeout time.Duration, + connection ServerConnection, password string, timeout time.Duration, ) error { err := ErrUnreachable done := make(chan bool) go func() { - _, err = s.testConnectionAndCreateClient(hostname, username, password, legacyAuth) + _, err = s.testConnectionAndCreateClient(connection, password) close(done) }() t := time.NewTimer(timeout) @@ -57,16 +58,10 @@ func (s *ServerManager) TestConnectionAndAuth( } } -func (s *ServerManager) testConnectionAndCreateClient(hostname, username, password string, legacyAuth bool) (*subsonic.Client, error) { - cli := &subsonic.Client{ - Client: &http.Client{}, - BaseUrl: hostname, - User: username, - PasswordAuth: legacyAuth, - ClientName: "supersonic", - } - if !cli.Ping() { - return nil, ErrUnreachable +func (s *ServerManager) testConnectionAndCreateClient(connection ServerConnection, password string) (*subsonic.Client, error) { + cli, err := s.connect(connection, password) + if err != nil { + return nil, err } if err := cli.Authenticate(password); err != nil { return nil, err @@ -74,6 +69,46 @@ func (s *ServerManager) testConnectionAndCreateClient(hostname, username, passwo return cli, nil } +func (s *ServerManager) connect(connection ServerConnection, password string) (*subsonic.Client, error) { + cli := &subsonic.Client{ + Client: &http.Client{}, + BaseUrl: connection.Hostname, + User: connection.Username, + PasswordAuth: connection.LegacyAuth, + ClientName: "supersonic", + } + altCli := &subsonic.Client{ + Client: &http.Client{}, + BaseUrl: connection.AltHostname, + User: connection.Username, + PasswordAuth: connection.LegacyAuth, + ClientName: "supersonic", + } + pingChan := make(chan bool, 2) // false for primary hostname, true for alternate + pingFunc := func(delay time.Duration, cli *subsonic.Client, val bool) { + <-time.After(delay) + if cli.Ping() { + pingChan <- val + } + } + go pingFunc(0, cli, false) + if connection.AltHostname != "" { + go pingFunc(333*time.Millisecond, altCli, true) // give primary hostname ping a head start + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + select { + case <-ctx.Done(): + return nil, ErrUnreachable + case altPing := <-pingChan: + if altPing { + return altCli, nil + } + return cli, nil + } +} + func (s *ServerManager) Logout() { if s.Server != nil { keyring.Delete(s.appName, s.ServerID.String()) diff --git a/ui/controller/controller.go b/ui/controller/controller.go index ba12958..9d1dd3d 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -132,7 +132,13 @@ func (m *Controller) PromptForFirstServer() { // connection is good pop.Hide() m.doModalClosed() - server := m.App.Config.AddServer(d.Nickname, d.Host, d.Username, d.LegacyAuth) + conn := backend.ServerConnection{ + Hostname: d.Host, + AltHostname: d.AltHost, + Username: d.Username, + LegacyAuth: d.LegacyAuth, + } + server := m.App.Config.AddServer(d.Nickname, conn) if err := m.App.ServerManager.SetServerPassword(server, d.Password); err != nil { log.Printf("error setting keyring credentials: %v", err) // TODO: handle? @@ -254,7 +260,7 @@ func (m *Controller) PromptForLoginAndConnect() { d.DisableSubmit() d.SetInfoText("Testing connection...") go func() { - err := m.App.ServerManager.TestConnectionAndAuth(server.Hostname, server.Username, password, server.LegacyAuth, 5*time.Second) + err := m.App.ServerManager.TestConnectionAndAuth(server.ServerConnection, password, 5*time.Second) if err == backend.ErrUnreachable { d.SetErrorText("Server unreachable") } else if err != nil { @@ -278,6 +284,7 @@ func (m *Controller) PromptForLoginAndConnect() { // connection is good editPop.Hide() server.Hostname = editD.Host + server.AltHostname = editD.AltHost server.Nickname = editD.Nickname server.Username = editD.Username server.LegacyAuth = editD.LegacyAuth @@ -343,7 +350,7 @@ func (c *Controller) trySetPasswordAndConnectToServer(server *backend.ServerConf } func (c *Controller) tryConnectToServer(server *backend.ServerConfig, password string) error { - if err := c.App.ServerManager.TestConnectionAndAuth(server.Hostname, server.Username, password, server.LegacyAuth, 10*time.Second); err != nil { + if err := c.App.ServerManager.TestConnectionAndAuth(server.ServerConnection, password, 10*time.Second); err != nil { return err } if err := c.App.ServerManager.ConnectToServer(server, password); err != nil { @@ -355,7 +362,13 @@ func (c *Controller) tryConnectToServer(server *backend.ServerConfig, password s func (c *Controller) testConnectionAndUpdateDialogText(dlg *dialogs.AddEditServerDialog) bool { dlg.SetInfoText("Testing connection...") - err := c.App.ServerManager.TestConnectionAndAuth(dlg.Host, dlg.Username, dlg.Password, dlg.LegacyAuth, 5*time.Second) + conn := backend.ServerConnection{ + Hostname: dlg.Host, + AltHostname: dlg.AltHost, + Username: dlg.Username, + LegacyAuth: dlg.LegacyAuth, + } + err := c.App.ServerManager.TestConnectionAndAuth(conn, dlg.Password, 5*time.Second) if err == backend.ErrUnreachable { dlg.SetErrorText("Could not reach server (wrong hostname?)") return false diff --git a/ui/dialogs/addeditserverdialog.go b/ui/dialogs/addeditserverdialog.go index 3fa95f1..e789cb9 100644 --- a/ui/dialogs/addeditserverdialog.go +++ b/ui/dialogs/addeditserverdialog.go @@ -16,6 +16,7 @@ type AddEditServerDialog struct { Nickname string Host string + AltHost string Username string Password string LegacyAuth bool @@ -34,6 +35,7 @@ func NewAddEditServerDialog(title string, prefillServer *backend.ServerConfig) * if prefillServer != nil { a.Nickname = prefillServer.Nickname a.Host = prefillServer.Hostname + a.AltHost = prefillServer.AltHostname a.Username = prefillServer.Username a.LegacyAuth = prefillServer.LegacyAuth } @@ -44,6 +46,8 @@ func NewAddEditServerDialog(title string, prefillServer *backend.ServerConfig) * nickField.SetPlaceHolder("My Server") hostField := widget.NewEntryWithData(binding.BindString(&a.Host)) hostField.SetPlaceHolder("http://localhost:4533") + altHostField := widget.NewEntryWithData(binding.BindString(&a.AltHost)) + altHostField.SetPlaceHolder("(optional) https://my-external-domain.net/music") userField := widget.NewEntryWithData(binding.BindString(&a.Username)) passField := widget.NewPasswordEntry() a.submitBtn = widget.NewButton("Enter", func() { @@ -64,6 +68,8 @@ func NewAddEditServerDialog(title string, prefillServer *backend.ServerConfig) * nickField, widget.NewLabel("Hostname"), hostField, + widget.NewLabel("Alt. Hostname"), + altHostField, widget.NewLabel("Username"), userField, widget.NewLabel("Password"), @@ -111,7 +117,7 @@ func (a *AddEditServerDialog) doSetPromptText(text string, color fyne.ThemeColor func (a *AddEditServerDialog) MinSize() fyne.Size { a.ExtendBaseWidget(a) - return fyne.NewSize(450, a.container.MinSize().Height) + return fyne.NewSize(475, a.container.MinSize().Height) } func (a *AddEditServerDialog) CreateRenderer() fyne.WidgetRenderer {