-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
172 lines (152 loc) · 4.62 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
package main
import (
"errors"
"fmt"
"net/http"
"os"
"github.com/aymanbagabas/go-osc52/v2"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/huh/spinner"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/log"
"github.com/jon4hz/songlinkrr/config"
"github.com/jon4hz/songlinkrr/player"
"github.com/jon4hz/songlinkrr/player/plex"
"github.com/jon4hz/songlinkrr/version"
"github.com/muesli/termenv"
"github.com/samber/lo"
"github.com/spf13/cobra"
"github.com/supersonic-app/go-subsonic/subsonic"
)
var rootCmd = &cobra.Command{
Use: "songlinkrr",
Short: "Songlinkrr is a CLI tool to get song links for your currently playing song on Plex",
Run: root,
}
var rootCmdFlags struct {
configFile string
forceConfirm bool
}
func init() {
rootCmd.Flags().StringVarP(&rootCmdFlags.configFile, "config", "c", "", "path to the config file")
rootCmd.Flags().BoolVarP(&rootCmdFlags.forceConfirm, "force-confirm", "f", false, "force confirmation of the song")
}
func main() {
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
}
func root(cmd *cobra.Command, _ []string) {
lipgloss.SetColorProfile(termenv.TrueColor)
cfg, err := config.Load(rootCmdFlags.configFile)
if err != nil {
log.Fatal("Failed to load config", "err", err)
}
if cfg.PlexConfig.Username == "" {
log.Fatal("No client configured")
}
var client player.Player
if cfg.PlexConfig.Username != "" {
client = plex.New(&cfg.PlexConfig, cfg.PlexConfig.Username)
}
var sessions []*player.Session
if err := spinner.New().
Type(spinner.Dots).
Title("Fetching Plex sessions").
Action(func() {
var err error
sessions, err = client.GetSessions(cmd.Context())
if err != nil && errors.Is(err, player.ErrNoSessions) {
log.Info(err)
return
} else if err != nil {
log.Fatal("Failed to get Plex sessions", "err", err)
}
}).Run(); err != nil {
log.Fatal("Failed to run spinner", "err", err)
}
var session *player.Session
if rootCmdFlags.forceConfirm || len(sessions) > 1 {
if err := huh.NewSelect[*player.Session]().
Title("Select which song to share from Plex").
Options(lo.Map(sessions, func(s *player.Session, _ int) huh.Option[*player.Session] {
return huh.NewOption(
fmt.Sprintf("%s • %s (%s)", s.Artist, s.Title, s.Player),
s,
)
})...).
Value(&session).
Run(); err != nil {
log.Fatal("Failed to run form", "err", err)
}
} else if len(sessions) == 1 {
session = sessions[0]
} else {
log.Fatalf("No active music session found on %s", client.String())
}
subsonicClient := &subsonic.Client{
Client: http.DefaultClient,
BaseUrl: cfg.SubsonicConfig.URL,
User: cfg.SubsonicConfig.User,
ClientName: "songlinkrr-" + version.Version,
}
if err := subsonicClient.Authenticate(cfg.SubsonicConfig.Password); err != nil {
log.Fatal("Failed to authenticate to subsonic", "url", cfg.SubsonicConfig.URL, "err", err)
}
searchString := fmt.Sprintf("%s %s", session.Artist, session.Title)
RetrySearch:
var searchResult *subsonic.SearchResult3
if err := spinner.New().
Type(spinner.Dots).
Title("Searching for song on subsonic").
Action(func() {
var err error
searchResult, err = subsonicClient.Search3(searchString, nil)
if err != nil {
log.Fatal("Failed to search for song", "err", err)
}
}).Run(); err != nil {
log.Fatal("Failed to run spinner", "err", err)
}
if len(searchResult.Song) == 0 {
log.Warn("No matching songs found on subsonic", "query", searchString)
if err := huh.NewInput().
Title("Adjust search query").
Description("Or press ctrl+c to exit").
Value(&searchString).
Run(); err != nil {
log.Fatal("Failed to get search query", "err", err)
}
goto RetrySearch
}
var song *subsonic.Child
if err := huh.NewSelect[*subsonic.Child]().
Title("Select best match from subsonic").
Description(fmt.Sprintf("Search: %s", searchString)).
Options(lo.Map(searchResult.Song, func(s *subsonic.Child, _ int) huh.Option[*subsonic.Child] {
return huh.NewOption(fmt.Sprintf("%s • %s", s.Artist, s.Title), s)
})...).
Value(&song).
Run(); err != nil {
log.Fatal("Failed to select song", "err", err)
}
share, err := subsonicClient.CreateShare(song.ID, nil)
if err != nil {
log.Fatal("Failed to create share link", "err", err)
}
doCopy := true
if err := huh.NewConfirm().
Title("Copy share link to clipboard?").
Affirmative("Sure!").
Negative("Nope.").
Value(&doCopy).
Run(); err != nil {
log.Fatal("Failed to confirm", "err", err)
}
if doCopy {
if _, err := osc52.New(share.Url).WriteTo(os.Stderr); err != nil {
log.Fatal("Failed to copy share link to clipboard", "err", err)
}
}
fmt.Println(share.Url)
}