From 5d67e5c43c9123b2508c0b4840def4738744a4d6 Mon Sep 17 00:00:00 2001 From: Simon Garrelou Date: Mon, 5 Dec 2022 19:44:07 +0100 Subject: Rework code organization + add README --- README.md | 20 ++++++++++ cmd/main.go | 39 ++++++++++++++++++ go.mod | 1 + go.sum | 2 + main.go | 112 ---------------------------------------------------- src/alert.go | 18 +++++++++ src/app.go | 72 +++++++++++++++++++++++++++++++++ src/config.go | 90 +++++++++++++++++++++++++++++++++++++++++ src/page_artists.go | 15 +++++++ src/page_config.go | 51 ++++++++++++++++++++++++ 10 files changed, 308 insertions(+), 112 deletions(-) create mode 100644 README.md create mode 100644 cmd/main.go delete mode 100644 main.go create mode 100644 src/alert.go create mode 100644 src/app.go create mode 100644 src/config.go create mode 100644 src/page_artists.go create mode 100644 src/page_config.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..6372bb6 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# termsonic - a TUI Subsonic client + +This project implements a terminal-based client for any [Subsonic](https://www.subsonic.org)-compatible server. + +## Building + +This application requires [Go](https://go.dev) version 1.19 at minimum. + +``` +$ git clone https://git.sixfoisneuf.fr/termsonic && cd termsonic +$ go build -o termsonic ./cmd +``` + +## Configuration + +The application reads its configuration from `$XDG_CONFIG_DIR/termsonic.toml`, or `~/.config/termsonic.toml` if `XDG_CONFIG_DIR` doesn't exist. + +On Windows, it reads its configuration from `%APPDATA%\\Termsonic\\termsonic.toml`. + +You can edit the configuration from inside the app, or by passing parameters on the command line (see `--help`). \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..53874e5 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "git.sixfoisneuf.fr/simon/termsonic/src" +) + +var ( + baseURL = flag.String("url", "", "URL to your Subsonic server") + username = flag.String("username", "", "Subsonic username") + password = flag.String("password", "", "Subsonic password") +) + +func main() { + flag.Parse() + + cfg, err := src.LoadDefaultConfig() + if err != nil { + fmt.Printf("Could not start termsonic: %v", err) + os.Exit(1) + } + + if *baseURL != "" { + cfg.BaseURL = *baseURL + } + + if *username != "" { + cfg.Username = *username + } + + if *password != "" { + cfg.Password = *password + } + + src.Run(cfg) +} diff --git a/go.mod b/go.mod index d1241d5..3a9d2e8 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module git.sixfoisneuf.fr/simon/termsonic go 1.19 require ( + github.com/BurntSushi/toml v1.2.1 // indirect github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5 // indirect github.com/gdamore/encoding v1.0.0 // indirect github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 // indirect diff --git a/go.sum b/go.sum index 396fa5d..43494fb 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5 h1:RuuxidatioSKGOiBzL1mTY4X22DQD8weEbS3iRLHnAg= github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5/go.mod h1:vnbEuj6Z20PLcHB4rrLQAOXGMjtULfMGhRVSFPcSdUo= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= diff --git a/main.go b/main.go deleted file mode 100644 index ad0d7c8..0000000 --- a/main.go +++ /dev/null @@ -1,112 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "os" - - "github.com/delucks/go-subsonic" - "github.com/rivo/tview" -) - -var ( - url = "" - username = "" - password = "" - - sub *subsonic.Client = nil - - // GUI - app *tview.Application - pages *tview.Pages -) - -func main() { - loadConfig() - - app = tview.NewApplication() - - pages = tview.NewPages() - - showConfig := sub == nil - pages.AddPage("page-config", configPage(), true, showConfig) - - if !showConfig { - pages.AddPage("page-main", mainView(), true, true) - } - - if err := app.SetRoot(pages, true).EnableMouse(true).SetFocus(pages).Run(); err != nil { - fmt.Printf("Error running TermSonic: %v", err) - os.Exit(1) - } -} - -func configPage() *tview.Form { - form := tview.NewForm(). - AddInputField("Server URL", url, 40, nil, func(txt string) { url = txt }). - AddInputField("Username", username, 20, nil, func(txt string) { username = txt }). - AddPasswordField("Password", password, 20, '*', func(txt string) { password = txt }). - AddButton("Test", func() { - tmpSub := &subsonic.Client{ - Client: http.DefaultClient, - BaseUrl: url, - User: username, - ClientName: "termsonic", - PasswordAuth: true, - } - - if err := tmpSub.Authenticate(password); err != nil { - alert("Could not auth: %v", err) - } else { - sub = tmpSub - - alert("Success.") - } - }). - AddButton("Save", nil) - return form -} - -func mainView() *tview.Grid { - grid := tview.NewGrid(). - SetRows(2, 0). - SetColumns(30, 0). - SetBorders(true) - - grid.AddItem(tview.NewTextView().SetText("Artist & Album list"), 1, 0, 1, 1, 0, 0, true) - grid.AddItem(tview.NewTextView().SetText("Song list!"), 1, 1, 1, 2, 0, 0, false) - - return grid -} - -func alert(format string, params ...interface{}) { - modal := tview.NewModal(). - SetText(fmt.Sprintf(format, params...)). - AddButtons([]string{"OK"}). - SetDoneFunc(func(_ int, _ string) { - pages.RemovePage("alert") - }) - - pages.AddPage("alert", modal, true, true) -} - -func loadConfig() { - url = "http://music.nuc.local" - username = "admin" - password = "admin" - - sub = &subsonic.Client{ - Client: http.DefaultClient, - User: username, - BaseUrl: url, - ClientName: "termsonic", - PasswordAuth: true, - } - - if err := sub.Authenticate(password); err != nil { - sub = nil - } -} - -func saveConfig() { -} diff --git a/src/alert.go b/src/alert.go new file mode 100644 index 0000000..917d089 --- /dev/null +++ b/src/alert.go @@ -0,0 +1,18 @@ +package src + +import ( + "fmt" + + "github.com/rivo/tview" +) + +func alert(a *app, format string, params ...interface{}) { + modal := tview.NewModal(). + SetText(fmt.Sprintf(format, params...)). + AddButtons([]string{"OK"}). + SetDoneFunc(func(_ int, _ string) { + a.pages.RemovePage("alert") + }) + + a.pages.AddPage("alert", modal, true, true) +} diff --git a/src/app.go b/src/app.go new file mode 100644 index 0000000..c18c1f5 --- /dev/null +++ b/src/app.go @@ -0,0 +1,72 @@ +package src + +import ( + "fmt" + "os" + + "github.com/delucks/go-subsonic" + "github.com/rivo/tview" +) + +type app struct { + tv *tview.Application + pages *tview.Pages + header *tview.TextView + footer *tview.TextView + cfg *Config + + sub *subsonic.Client +} + +func Run(cfg *Config) { + a := &app{ + cfg: cfg, + } + + a.tv = tview.NewApplication() + a.pages = tview.NewPages() + a.footer = tview.NewTextView() + + a.header = tview.NewTextView(). + SetRegions(true). + SetChangedFunc(func() { + a.tv.Draw() + }). + SetHighlightedFunc(func(added, removed, remaining []string) { + hl := added[0] + cur, _ := a.pages.GetFrontPage() + + if hl != cur { + switchToPage(a, hl) + } + }) + fmt.Fprintf(a.header, `["artists"]F1: Artists[""] | ["playlists"]F2: Playlists[""] | ["config"]F3: Configuration[""]`) + + a.pages.AddPage("config", configPage(a), true, false) + a.pages.AddPage("artists", artistsPage(a), true, false) + + mainLayout := tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(a.header, 0, 1, false). + AddItem(a.pages, 0, 3, true). + AddItem(a.footer, 0, 1, false) + + switchToPage(a, "config") + if err := a.tv.SetRoot(mainLayout, true).EnableMouse(true).SetFocus(mainLayout).Run(); err != nil { + fmt.Printf("Error running termsonic: %v", err) + os.Exit(1) + } +} + +func switchToPage(a *app, name string) { + if name == "artists" { + a.pages.SwitchToPage("artists") + a.header.Highlight("artists") + } else if name == "playlists" { + a.pages.SwitchToPage("playlists") + a.header.Highlight("playlists") + } else if name == "config" { + a.pages.SwitchToPage("config") + a.header.Highlight("config") + } +} diff --git a/src/config.go b/src/config.go new file mode 100644 index 0000000..1b4a998 --- /dev/null +++ b/src/config.go @@ -0,0 +1,90 @@ +package src + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + + "github.com/BurntSushi/toml" +) + +type Config struct { + BaseURL string + Username string + Password string +} + +func LoadConfigFromFile(filename string) (*Config, error) { + var cfg Config + _, err := toml.DecodeFile(filename, &cfg) + + return &cfg, err +} + +func LoadDefaultConfig() (*Config, error) { + path, err := getConfigFilePath() + if err != nil { + return nil, err + } + + f, err := os.Open(path) + if err != nil && !os.IsNotExist(err) { + return nil, err + } else if os.IsNotExist(err) { + return &Config{}, nil + } + f.Close() + + return LoadConfigFromFile(path) +} + +func getConfigFilePath() (string, error) { + path := "" + if runtime.GOOS == "linux" { + configDir := os.Getenv("XDG_CONFIG_DIR") + if configDir == "" { + home := os.Getenv("HOME") + if home == "" { + return "", fmt.Errorf("could not determine where to store configuration") + } + + path = filepath.Join(home, ".config") + os.MkdirAll(path, os.ModeDir.Perm()) + + path = filepath.Join(path, "termsonic.toml") + } else { + path = filepath.Join(configDir, "termsonic.toml") + } + } else if runtime.GOOS == "windows" { + appdata := os.Getenv("APPDATA") + if appdata == "" { + return "", fmt.Errorf("could not find %%APPDATA%%") + } + + path = filepath.Join(appdata, "Termsonic") + os.MkdirAll(path, os.ModeDir.Perm()) + + path = filepath.Join(path, "termsonic.toml") + } else { + return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS) + } + + return path, nil +} + +func (c *Config) Save() error { + path, err := getConfigFilePath() + if err != nil { + return err + } + + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + + enc := toml.NewEncoder(f) + return enc.Encode(*c) +} diff --git a/src/page_artists.go b/src/page_artists.go new file mode 100644 index 0000000..6b6def6 --- /dev/null +++ b/src/page_artists.go @@ -0,0 +1,15 @@ +package src + +import "github.com/rivo/tview" + +func artistsPage(a *app) tview.Primitive { + grid := tview.NewGrid(). + SetRows(1). + SetColumns(30, 0). + SetBorders(true) + + grid.AddItem(tview.NewTextView().SetText("Artist & Album list"), 0, 0, 1, 1, 0, 0, true) + grid.AddItem(tview.NewTextView().SetText("Song list!"), 0, 1, 1, 2, 0, 0, false) + + return grid +} diff --git a/src/page_config.go b/src/page_config.go new file mode 100644 index 0000000..bb8afca --- /dev/null +++ b/src/page_config.go @@ -0,0 +1,51 @@ +package src + +import ( + "net/http" + + "github.com/delucks/go-subsonic" + "github.com/rivo/tview" +) + +func configPage(a *app) *tview.Form { + form := tview.NewForm(). + AddInputField("Server URL", a.cfg.BaseURL, 40, nil, func(txt string) { a.cfg.BaseURL = txt }). + AddInputField("Username", a.cfg.Username, 20, nil, func(txt string) { a.cfg.Username = txt }). + AddPasswordField("Password", a.cfg.Password, 20, '*', func(txt string) { a.cfg.Password = txt }). + AddButton("Test", func() { + tmpSub := &subsonic.Client{ + Client: http.DefaultClient, + BaseUrl: a.cfg.BaseURL, + User: a.cfg.Username, + ClientName: "termsonic", + PasswordAuth: true, + } + + if err := tmpSub.Authenticate(a.cfg.Password); err != nil { + alert(a, "Could not auth: %v", err) + } else { + alert(a, "Success.") + } + }). + AddButton("Save", func() { + err := a.cfg.Save() + if err != nil { + alert(a, "Error saving: %v", err) + return + } + + a.sub = &subsonic.Client{ + Client: http.DefaultClient, + BaseUrl: a.cfg.BaseURL, + User: a.cfg.Username, + ClientName: "termsonic", + PasswordAuth: true, + } + if err := a.sub.Authenticate(a.cfg.Password); err != nil { + alert(a, "Could not auth: %v", err) + } else { + alert(a, "All good!") + } + }) + return form +} -- cgit v1.2.3