aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimon Garrelou <simon.garrelou@gmail.com>2022-12-05 19:44:07 +0100
committerSimon Garrelou <simon.garrelou@gmail.com>2022-12-05 19:44:07 +0100
commit5d67e5c43c9123b2508c0b4840def4738744a4d6 (patch)
tree4a4342cefc8066133cac49d884563a9bdd2d8b23
parent8bcd996e28572f2362d186c6e2bbb3971462feee (diff)
downloadtermsonic-5d67e5c43c9123b2508c0b4840def4738744a4d6.tar.gz
termsonic-5d67e5c43c9123b2508c0b4840def4738744a4d6.zip
Rework code organization + add README
-rw-r--r--README.md20
-rw-r--r--cmd/main.go39
-rw-r--r--go.mod1
-rw-r--r--go.sum2
-rw-r--r--main.go112
-rw-r--r--src/alert.go18
-rw-r--r--src/app.go72
-rw-r--r--src/config.go90
-rw-r--r--src/page_artists.go15
-rw-r--r--src/page_config.go51
10 files changed, 308 insertions, 112 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6372bb6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,20 @@
1# termsonic - a TUI Subsonic client
2
3This project implements a terminal-based client for any [Subsonic](https://www.subsonic.org)-compatible server.
4
5## Building
6
7This application requires [Go](https://go.dev) version 1.19 at minimum.
8
9```
10$ git clone https://git.sixfoisneuf.fr/termsonic && cd termsonic
11$ go build -o termsonic ./cmd
12```
13
14## Configuration
15
16The application reads its configuration from `$XDG_CONFIG_DIR/termsonic.toml`, or `~/.config/termsonic.toml` if `XDG_CONFIG_DIR` doesn't exist.
17
18On Windows, it reads its configuration from `%APPDATA%\\Termsonic\\termsonic.toml`.
19
20You 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 @@
1package main
2
3import (
4 "flag"
5 "fmt"
6 "os"
7
8 "git.sixfoisneuf.fr/simon/termsonic/src"
9)
10
11var (
12 baseURL = flag.String("url", "", "URL to your Subsonic server")
13 username = flag.String("username", "", "Subsonic username")
14 password = flag.String("password", "", "Subsonic password")
15)
16
17func main() {
18 flag.Parse()
19
20 cfg, err := src.LoadDefaultConfig()
21 if err != nil {
22 fmt.Printf("Could not start termsonic: %v", err)
23 os.Exit(1)
24 }
25
26 if *baseURL != "" {
27 cfg.BaseURL = *baseURL
28 }
29
30 if *username != "" {
31 cfg.Username = *username
32 }
33
34 if *password != "" {
35 cfg.Password = *password
36 }
37
38 src.Run(cfg)
39}
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
3go 1.19 3go 1.19
4 4
5require ( 5require (
6 github.com/BurntSushi/toml v1.2.1 // indirect
6 github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5 // indirect 7 github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5 // indirect
7 github.com/gdamore/encoding v1.0.0 // indirect 8 github.com/gdamore/encoding v1.0.0 // indirect
8 github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 // indirect 9 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 @@
1github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
2github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
1github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5 h1:RuuxidatioSKGOiBzL1mTY4X22DQD8weEbS3iRLHnAg= 3github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5 h1:RuuxidatioSKGOiBzL1mTY4X22DQD8weEbS3iRLHnAg=
2github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5/go.mod h1:vnbEuj6Z20PLcHB4rrLQAOXGMjtULfMGhRVSFPcSdUo= 4github.com/delucks/go-subsonic v0.0.0-20220915164742-2744002c4be5/go.mod h1:vnbEuj6Z20PLcHB4rrLQAOXGMjtULfMGhRVSFPcSdUo=
3github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= 5github.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 @@
1package main
2
3import (
4 "fmt"
5 "net/http"
6 "os"
7
8 "github.com/delucks/go-subsonic"
9 "github.com/rivo/tview"
10)
11
12var (
13 url = ""
14 username = ""
15 password = ""
16
17 sub *subsonic.Client = nil
18
19 // GUI
20 app *tview.Application
21 pages *tview.Pages
22)
23
24func main() {
25 loadConfig()
26
27 app = tview.NewApplication()
28
29 pages = tview.NewPages()
30
31 showConfig := sub == nil
32 pages.AddPage("page-config", configPage(), true, showConfig)
33
34 if !showConfig {
35 pages.AddPage("page-main", mainView(), true, true)
36 }
37
38 if err := app.SetRoot(pages, true).EnableMouse(true).SetFocus(pages).Run(); err != nil {
39 fmt.Printf("Error running TermSonic: %v", err)
40 os.Exit(1)
41 }
42}
43
44func configPage() *tview.Form {
45 form := tview.NewForm().
46 AddInputField("Server URL", url, 40, nil, func(txt string) { url = txt }).
47 AddInputField("Username", username, 20, nil, func(txt string) { username = txt }).
48 AddPasswordField("Password", password, 20, '*', func(txt string) { password = txt }).
49 AddButton("Test", func() {
50 tmpSub := &subsonic.Client{
51 Client: http.DefaultClient,
52 BaseUrl: url,
53 User: username,
54 ClientName: "termsonic",
55 PasswordAuth: true,
56 }
57
58 if err := tmpSub.Authenticate(password); err != nil {
59 alert("Could not auth: %v", err)
60 } else {
61 sub = tmpSub
62
63 alert("Success.")
64 }
65 }).
66 AddButton("Save", nil)
67 return form
68}
69
70func mainView() *tview.Grid {
71 grid := tview.NewGrid().
72 SetRows(2, 0).
73 SetColumns(30, 0).
74 SetBorders(true)
75
76 grid.AddItem(tview.NewTextView().SetText("Artist & Album list"), 1, 0, 1, 1, 0, 0, true)
77 grid.AddItem(tview.NewTextView().SetText("Song list!"), 1, 1, 1, 2, 0, 0, false)
78
79 return grid
80}
81
82func alert(format string, params ...interface{}) {
83 modal := tview.NewModal().
84 SetText(fmt.Sprintf(format, params...)).
85 AddButtons([]string{"OK"}).
86 SetDoneFunc(func(_ int, _ string) {
87 pages.RemovePage("alert")
88 })
89
90 pages.AddPage("alert", modal, true, true)
91}
92
93func loadConfig() {
94 url = "http://music.nuc.local"
95 username = "admin"
96 password = "admin"
97
98 sub = &subsonic.Client{
99 Client: http.DefaultClient,
100 User: username,
101 BaseUrl: url,
102 ClientName: "termsonic",
103 PasswordAuth: true,
104 }
105
106 if err := sub.Authenticate(password); err != nil {
107 sub = nil
108 }
109}
110
111func saveConfig() {
112}
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 @@
1package src
2
3import (
4 "fmt"
5
6 "github.com/rivo/tview"
7)
8
9func alert(a *app, format string, params ...interface{}) {
10 modal := tview.NewModal().
11 SetText(fmt.Sprintf(format, params...)).
12 AddButtons([]string{"OK"}).
13 SetDoneFunc(func(_ int, _ string) {
14 a.pages.RemovePage("alert")
15 })
16
17 a.pages.AddPage("alert", modal, true, true)
18}
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 @@
1package src
2
3import (
4 "fmt"
5 "os"
6
7 "github.com/delucks/go-subsonic"
8 "github.com/rivo/tview"
9)
10
11type app struct {
12 tv *tview.Application
13 pages *tview.Pages
14 header *tview.TextView
15 footer *tview.TextView
16 cfg *Config
17
18 sub *subsonic.Client
19}
20
21func Run(cfg *Config) {
22 a := &app{
23 cfg: cfg,
24 }
25
26 a.tv = tview.NewApplication()
27 a.pages = tview.NewPages()
28 a.footer = tview.NewTextView()
29
30 a.header = tview.NewTextView().
31 SetRegions(true).
32 SetChangedFunc(func() {
33 a.tv.Draw()
34 }).
35 SetHighlightedFunc(func(added, removed, remaining []string) {
36 hl := added[0]
37 cur, _ := a.pages.GetFrontPage()
38
39 if hl != cur {
40 switchToPage(a, hl)
41 }
42 })
43 fmt.Fprintf(a.header, `["artists"]F1: Artists[""] | ["playlists"]F2: Playlists[""] | ["config"]F3: Configuration[""]`)
44
45 a.pages.AddPage("config", configPage(a), true, false)
46 a.pages.AddPage("artists", artistsPage(a), true, false)
47
48 mainLayout := tview.NewFlex().
49 SetDirection(tview.FlexRow).
50 AddItem(a.header, 0, 1, false).
51 AddItem(a.pages, 0, 3, true).
52 AddItem(a.footer, 0, 1, false)
53
54 switchToPage(a, "config")
55 if err := a.tv.SetRoot(mainLayout, true).EnableMouse(true).SetFocus(mainLayout).Run(); err != nil {
56 fmt.Printf("Error running termsonic: %v", err)
57 os.Exit(1)
58 }
59}
60
61func switchToPage(a *app, name string) {
62 if name == "artists" {
63 a.pages.SwitchToPage("artists")
64 a.header.Highlight("artists")
65 } else if name == "playlists" {
66 a.pages.SwitchToPage("playlists")
67 a.header.Highlight("playlists")
68 } else if name == "config" {
69 a.pages.SwitchToPage("config")
70 a.header.Highlight("config")
71 }
72}
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 @@
1package src
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "runtime"
8
9 "github.com/BurntSushi/toml"
10)
11
12type Config struct {
13 BaseURL string
14 Username string
15 Password string
16}
17
18func LoadConfigFromFile(filename string) (*Config, error) {
19 var cfg Config
20 _, err := toml.DecodeFile(filename, &cfg)
21
22 return &cfg, err
23}
24
25func LoadDefaultConfig() (*Config, error) {
26 path, err := getConfigFilePath()
27 if err != nil {
28 return nil, err
29 }
30
31 f, err := os.Open(path)
32 if err != nil && !os.IsNotExist(err) {
33 return nil, err
34 } else if os.IsNotExist(err) {
35 return &Config{}, nil
36 }
37 f.Close()
38
39 return LoadConfigFromFile(path)
40}
41
42func getConfigFilePath() (string, error) {
43 path := ""
44 if runtime.GOOS == "linux" {
45 configDir := os.Getenv("XDG_CONFIG_DIR")
46 if configDir == "" {
47 home := os.Getenv("HOME")
48 if home == "" {
49 return "", fmt.Errorf("could not determine where to store configuration")
50 }
51
52 path = filepath.Join(home, ".config")
53 os.MkdirAll(path, os.ModeDir.Perm())
54
55 path = filepath.Join(path, "termsonic.toml")
56 } else {
57 path = filepath.Join(configDir, "termsonic.toml")
58 }
59 } else if runtime.GOOS == "windows" {
60 appdata := os.Getenv("APPDATA")
61 if appdata == "" {
62 return "", fmt.Errorf("could not find %%APPDATA%%")
63 }
64
65 path = filepath.Join(appdata, "Termsonic")
66 os.MkdirAll(path, os.ModeDir.Perm())
67
68 path = filepath.Join(path, "termsonic.toml")
69 } else {
70 return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
71 }
72
73 return path, nil
74}
75
76func (c *Config) Save() error {
77 path, err := getConfigFilePath()
78 if err != nil {
79 return err
80 }
81
82 f, err := os.Create(path)
83 if err != nil {
84 return err
85 }
86 defer f.Close()
87
88 enc := toml.NewEncoder(f)
89 return enc.Encode(*c)
90}
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 @@
1package src
2
3import "github.com/rivo/tview"
4
5func artistsPage(a *app) tview.Primitive {
6 grid := tview.NewGrid().
7 SetRows(1).
8 SetColumns(30, 0).
9 SetBorders(true)
10
11 grid.AddItem(tview.NewTextView().SetText("Artist & Album list"), 0, 0, 1, 1, 0, 0, true)
12 grid.AddItem(tview.NewTextView().SetText("Song list!"), 0, 1, 1, 2, 0, 0, false)
13
14 return grid
15}
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 @@
1package src
2
3import (
4 "net/http"
5
6 "github.com/delucks/go-subsonic"
7 "github.com/rivo/tview"
8)
9
10func configPage(a *app) *tview.Form {
11 form := tview.NewForm().
12 AddInputField("Server URL", a.cfg.BaseURL, 40, nil, func(txt string) { a.cfg.BaseURL = txt }).
13 AddInputField("Username", a.cfg.Username, 20, nil, func(txt string) { a.cfg.Username = txt }).
14 AddPasswordField("Password", a.cfg.Password, 20, '*', func(txt string) { a.cfg.Password = txt }).
15 AddButton("Test", func() {
16 tmpSub := &subsonic.Client{
17 Client: http.DefaultClient,
18 BaseUrl: a.cfg.BaseURL,
19 User: a.cfg.Username,
20 ClientName: "termsonic",
21 PasswordAuth: true,
22 }
23
24 if err := tmpSub.Authenticate(a.cfg.Password); err != nil {
25 alert(a, "Could not auth: %v", err)
26 } else {
27 alert(a, "Success.")
28 }
29 }).
30 AddButton("Save", func() {
31 err := a.cfg.Save()
32 if err != nil {
33 alert(a, "Error saving: %v", err)
34 return
35 }
36
37 a.sub = &subsonic.Client{
38 Client: http.DefaultClient,
39 BaseUrl: a.cfg.BaseURL,
40 User: a.cfg.Username,
41 ClientName: "termsonic",
42 PasswordAuth: true,
43 }
44 if err := a.sub.Authenticate(a.cfg.Password); err != nil {
45 alert(a, "Could not auth: %v", err)
46 } else {
47 alert(a, "All good!")
48 }
49 })
50 return form
51}