diff --git a/cmd/pipe.go b/cmd/pipe.go index cb65aae..78ffbba 100644 --- a/cmd/pipe.go +++ b/cmd/pipe.go @@ -21,17 +21,17 @@ var pipeCmd = &cobra.Command{ if err != nil { return fmt.Errorf("couldn't load config: %w", err) } - player, err := loadPlayer(conf) + players, err := loadPlayers(conf) if err != nil { - return fmt.Errorf("couldn't load player: %w", err) + return fmt.Errorf("couldn't load players: %w", err) } - provider, err := loadProvider(conf, player) + provider, err := loadProvider(conf) if err != nil { return fmt.Errorf("couldn't load provider: %w", err) } ch := make(chan pool.Update) - go pool.Listen(player, provider, conf, ch) + go pool.Listen(players, provider, conf, ch) for update := range ch { printUpdate(update, conf) diff --git a/cmd/root.go b/cmd/root.go index 2b6e302..a1f2f5d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,6 +8,7 @@ import ( "sptlrx/lyrics" "sptlrx/player" "sptlrx/pool" + "sptlrx/services/combo" "sptlrx/services/hosted" "sptlrx/services/local" "sptlrx/services/spotify" @@ -35,7 +36,6 @@ const help = ` 1. Open your browser. var ( FlagCookie string - FlagPlayer string FlagConfig string FlagStyleBefore string @@ -58,17 +58,17 @@ var rootCmd = &cobra.Command{ if err != nil { return fmt.Errorf("couldn't load config: %w", err) } - player, err := loadPlayer(conf) + players, err := loadPlayers(conf) if err != nil { return fmt.Errorf("couldn't load player: %w", err) } - provider, err := loadProvider(conf, player) + provider, err := loadProvider(conf) if err != nil { return fmt.Errorf("couldn't load provider: %w", err) } ch := make(chan pool.Update) - go pool.Listen(player, provider, conf, ch) + go pool.Listen(players, provider, conf, ch) _, err = tea.NewProgram( &ui.Model{ @@ -109,9 +109,6 @@ func loadConfig(cmd *cobra.Command) (*config.Config, error) { conf.IgnoreErrors = false } - if cmd.Flags().Changed("player") { - conf.Player = FlagPlayer - } if cmd.Flags().Changed("before") { conf.Style.Before = parseStyleFlag(FlagStyleBefore) } @@ -127,8 +124,8 @@ func loadConfig(cmd *cobra.Command) (*config.Config, error) { return conf, nil } -func loadPlayer(conf *config.Config) (player.Player, error) { - player, err := config.GetPlayer(conf) +func loadPlayers(conf *config.Config) ([]*player.Player, error) { + players, err := config.GetPlayers(conf) if err != nil { if errors.Is(err, spotify.ErrInvalidCookie) { fmt.Println("If you want to use Spotify as your player, you need to set up your cookie.") @@ -136,22 +133,28 @@ func loadPlayer(conf *config.Config) (player.Player, error) { } return nil, err } - return player, nil + return players, nil } -func loadProvider(conf *config.Config, player player.Player) (lyrics.Provider, error) { - if conf.Local.Folder != "" { - return local.New(conf.Local.Folder) - } +func loadProvider(conf *config.Config) (lyrics.Provider, error) { + providers := []lyrics.Provider{} + if conf.Cookie == "" { - return hosted.New(conf.Host), nil + + coovieProv := hosted.New(conf.Host) + providers = append(providers, coovieProv) + } else { + var s, _ = spotify.NewProvider(conf.Cookie) + providers = append(providers, s) } - if spt, ok := player.(*spotify.Client); ok { - // use existing spotify client - return spt, nil + if conf.Local.Folder != "" { + localProv, err := local.New(conf.Local.Folder) + if err == nil { + providers = append(providers, localProv) + } + // return local.New(conf.Local.Folder) } - // create new spotify client - return spotify.New(conf.Cookie) + return combo.New(providers) } func parseStyleFlag(value string) config.Style { @@ -183,7 +186,6 @@ func parseStyleFlag(value string) config.Style { func init() { rootCmd.PersistentFlags().StringVarP(&FlagCookie, "cookie", "c", "", "your cookie") - rootCmd.PersistentFlags().StringVarP(&FlagPlayer, "player", "p", "spotify", "what player to use") rootCmd.PersistentFlags().StringVar(&FlagConfig, "config", config.Path, "path to config file") rootCmd.Flags().StringVar(&FlagStyleBefore, "before", "bold", "style of the lines before the current one") diff --git a/config/config.go b/config/config.go index 7febae9..dec7efb 100644 --- a/config/config.go +++ b/config/config.go @@ -31,12 +31,12 @@ func init() { } type Config struct { - Cookie string `yaml:"cookie"` - Player string `default:"spotify" yaml:"player"` - Host string `default:"lyricsapi.vercel.app" yaml:"host"` - IgnoreErrors bool `default:"true" yaml:"ignoreErrors"` - TimerInterval int `default:"200" yaml:"timerInterval"` - UpdateInterval int `default:"2000" yaml:"updateInterval"` + Cookie string `yaml:"cookie"` + Players []string `default:"[spotify]" yaml:"players"` + Host string `default:"lyricsapi.vercel.app" yaml:"host"` + IgnoreErrors bool `default:"true" yaml:"ignoreErrors"` + TimerInterval int `default:"200" yaml:"timerInterval"` + UpdateInterval int `default:"2000" yaml:"updateInterval"` Style struct { HAlignment string `default:"center" yaml:"hAlignment"` @@ -172,19 +172,32 @@ func validateColor(color string) bool { return false } -// GetPlayer returns a player based on config values -func GetPlayer(conf *Config) (player.Player, error) { - switch conf.Player { - case "spotify": - return spotify.New(conf.Cookie) - case "mpd": - return mpd.New(conf.Mpd.Address, conf.Mpd.Password), nil - case "mopidy": - return mopidy.New(conf.Mopidy.Address), nil - case "mpris": - return mpris.New(conf.Mpris.Players) - case "browser": - return browser.New(conf.Browser.Port) - } - return nil, fmt.Errorf("unknown player: \"%s\"", conf.Player) +// GetPlayers returns a player based on config values +func GetPlayers(conf *Config) ([]*player.Player, error) { + var players []*player.Player + for _, p := range conf.Players { + os.Stderr.WriteString("LOADR: Processing " + p) + switch p { + case "spotify": + spotifyPlayer, _ := spotify.NewPlayer(conf.Cookie) + players = append(players, &spotifyPlayer) + case "mpd": + mpdPlayer := mpd.New(conf.Mpd.Address, conf.Mpd.Password) + players = append(players, &mpdPlayer) + case "mopidy": + mopidyPlayer := mopidy.New(conf.Mopidy.Address) + players = append(players, &mopidyPlayer) + case "mpris": + mprisPlayer, _ := mpris.New(conf.Mpris.Players) + players = append(players, &mprisPlayer) + case "browser": + browserPlayer, _ := browser.New(conf.Browser.Port) + players = append(players, &browserPlayer) + } + } + if len(players) > 0 { + return players, nil + } + + return nil, fmt.Errorf("unknown players: \"%s\"", conf.Players) } diff --git a/lyrics/lyrics.go b/lyrics/lyrics.go index a1507fe..fad6f31 100644 --- a/lyrics/lyrics.go +++ b/lyrics/lyrics.go @@ -1,7 +1,10 @@ package lyrics +import "sptlrx/player" + type Provider interface { - Lyrics(id, query string) ([]Line, error) + Lyrics(state player.State) ([]Line, error) + Name() string } type Line struct { diff --git a/player/player.go b/player/player.go index a6ae10d..569d909 100644 --- a/player/player.go +++ b/player/player.go @@ -8,7 +8,11 @@ type State struct { // ID of the current track. ID string // Query is a string that can be used to find lyrics. - Query string + TrackNumber int + Artist string + Album string + Title string + SongPath string // Position of the current track in ms. Position int // Playing means whether the track is playing at the moment. diff --git a/pool/pool.go b/pool/pool.go index ecaea9c..fa0e4c9 100644 --- a/pool/pool.go +++ b/pool/pool.go @@ -18,13 +18,15 @@ type Update struct { // Listen polls for lyrics updates and writes them to the channel. func Listen( - player player.Player, + players []*player.Player, provider lyrics.Provider, conf *config.Config, ch chan Update, ) { stateCh := make(chan playerState) - go listenPlayer(player, stateCh, conf.UpdateInterval) + for _, p := range players { + go listenPlayer(*p, stateCh, conf.UpdateInterval) + } ticker := time.NewTicker( time.Millisecond * time.Duration(conf.TimerInterval), @@ -47,7 +49,7 @@ func Listen( if newState.ID != state.ID { changed = true if newState.ID != "" { - newLines, err := provider.Lyrics(newState.ID, newState.Query) + newLines, err := provider.Lyrics(newState.State) if err != nil { state.Err = err } @@ -100,12 +102,14 @@ func listenPlayer(player player.Player, ch chan playerState, interval int) { st := playerState{Err: err} if state != nil { st.ID = state.ID - st.Query = state.Query + st.Album = state.Album + st.Artist = state.Artist + st.Title = state.Title + st.TrackNumber = state.TrackNumber st.Playing = state.Playing st.Position = state.Position } ch <- st - time.Sleep(time.Millisecond * time.Duration(interval)) } } diff --git a/services/browser/browser.go b/services/browser/browser.go index 693b0fd..e03a4a2 100644 --- a/services/browser/browser.go +++ b/services/browser/browser.go @@ -5,6 +5,7 @@ import ( "io" "net" "net/http" + "os" "sptlrx/player" "strconv" "strings" @@ -24,7 +25,7 @@ const ( playing ) -func New(port int) (*Client, error) { +func New(port int) (player.Player, error) { c := &Client{} return c, c.start(port) } @@ -73,6 +74,8 @@ func (c *Client) handler(w http.ResponseWriter, r *http.Request) { if err != nil { return } + + os.Stderr.WriteString("BRWSR: msg: " + string(msg) + "\n") if t != websocket.MessageText || len(msg) == 0 { continue } @@ -81,6 +84,8 @@ func (c *Client) handler(w http.ResponseWriter, r *http.Request) { } func (c *Client) processMessage(msg string) { + + os.Stderr.WriteString("BRWSR: Recieved message" + "\n") spaceIndex := strings.IndexByte(msg, ' ') if spaceIndex == -1 { return @@ -140,21 +145,27 @@ func (c *Client) State() (*player.State, error) { if c.state == stopped { return nil, nil } + os.Stderr.WriteString("BRWSR: Found Song" + "\n") - var query string + var id string if c.artist != "" { - query = c.artist + " " + c.title + id = c.artist + " " + c.title } else { - query = c.title + id = c.title } + os.Stderr.WriteString("BRWSR: Artist" + c.artist + "\n") + os.Stderr.WriteString("BRWSR: Title" + c.title + "\n") + os.Stderr.WriteString("BRWSR: Position" + strconv.Itoa(c.position) + "\n") + position := c.position if c.state != paused { position += int(time.Since(c.updateTime).Milliseconds()) } return &player.State{ - ID: query, - Query: query, + ID: id, + Artist: c.artist, + Title: c.title, Position: position, Playing: c.state == playing, }, nil diff --git a/services/combo/combo.go b/services/combo/combo.go new file mode 100644 index 0000000..df522a8 --- /dev/null +++ b/services/combo/combo.go @@ -0,0 +1,32 @@ +package combo + +import ( + "sptlrx/lyrics" + "sptlrx/player" +) + +func New(providers []lyrics.Provider) (*ComboClient, error) { + + return &ComboClient{providers: providers}, nil +} + +type ComboClient struct { + providers []lyrics.Provider +} + +func (c *ComboClient) Lyrics(state player.State) ([]lyrics.Line, error) { + for _, p := range c.providers { + provider := p + newLines, err := provider.Lyrics(state) + if err != nil || newLines == nil { + continue + } else { + return newLines, nil + } + } + return nil, nil +} + +func (c *ComboClient) Name() string { + return "COMBO" +} diff --git a/services/hosted/hosted.go b/services/hosted/hosted.go index 3d17b8b..5dde553 100644 --- a/services/hosted/hosted.go +++ b/services/hosted/hosted.go @@ -5,11 +5,13 @@ import ( "fmt" "net/http" "net/url" + "os" "sptlrx/lyrics" + "sptlrx/player" ) // Host your own: https://github.com/raitonoberu/lyricsapi -func New(host string) *Client { +func New(host string) lyrics.Provider { return &Client{ host: host, } @@ -20,17 +22,38 @@ type Client struct { host string } -func (c *Client) Lyrics(id, query string) ([]lyrics.Line, error) { +func (c *Client) Lyrics(state player.State) ([]lyrics.Line, error) { + + query := state.Artist + " " + state.Title var url = fmt.Sprintf("https://%s/api/lyrics?name=%s", c.host, url.QueryEscape(query)) req, _ := http.NewRequest("GET", url, nil) resp, err := http.DefaultClient.Do(req) if err != nil { + os.Stderr.WriteString("HOSTD: Could not find lyrics" + "\n") return nil, err } defer resp.Body.Close() var result []lyrics.Line err = json.NewDecoder(resp.Body).Decode(&result) - return result, err + if len(result) > 0 { + // var header []lyrics.Line + // header = append(header, lyrics.Line{ + // Time: 0, + // Words: "Loading from Hosted...", + // }) + // if result[0].Time < 10 { + // result[0].Time = 10 + // } + os.Stderr.WriteString("HOSTD: Found Lyrics" + "\n") + return result, nil + } else { + os.Stderr.WriteString("HOSTD: Empty Lyrics" + "\n") + return nil, err + } +} + +func (c *Client) Name() string { + return "HOSTD" } diff --git a/services/local/local.go b/services/local/local.go index 163028c..52b4774 100644 --- a/services/local/local.go +++ b/services/local/local.go @@ -8,16 +8,18 @@ import ( "os" "path/filepath" "sptlrx/lyrics" + "sptlrx/player" "strconv" "strings" ) var replacer = strings.NewReplacer( "_", " ", "-", " ", - ",", "", ".", "", - "!", "", "?", "", - "(", "", ")", "", - "[", "", "]", "", + ",", " ", ".", " ", + "!", " ", "?", " ", + "(", " ", ")", " ", + "[", " ", "]", " ", + "/", " ", ) type file struct { @@ -25,7 +27,7 @@ type file struct { NameParts []string } -func New(folder string) (*Client, error) { +func New(folder string) (lyrics.Provider, error) { index, err := createIndex(folder) if err != nil { return nil, err @@ -38,27 +40,52 @@ type Client struct { index []*file } -func (c *Client) Lyrics(id, query string) ([]lyrics.Line, error) { - f := c.findFile(query) +func (c *Client) Lyrics(state player.State) ([]lyrics.Line, error) { + f := c.findFile(state) + if f == nil { + os.Stderr.WriteString("LOCAL: Could not find local lyrics" + "\n") return nil, nil } + os.Stderr.WriteString("LOCAL: Found lyrics at:" + f.Path + "\n") + reader, err := os.Open(f.Path) if err != nil { return nil, err } defer reader.Close() - return parseLrcFile(reader), nil + lys := parseLrcFile(reader) + // var header []lyrics.Line + // header = append(header, lyrics.Line{ + // Time: 0, + // Words: "Loading from Local...", + // }) + // if lys[0].Time < 10 { + // lys[0].Time = 10 + // } + return lys, nil } -func (c *Client) findFile(query string) *file { - parts := splitString(query) - +func (c *Client) findFile(state player.State) *file { + possiblePath := strings.Replace(strings.Replace(state.SongPath, ".mp3", ".lrc", 1), "file://", "", 1) var best *file + parts := splitString(state.Artist + " " + state.Album + " " + strconv.Itoa(state.TrackNumber) + " " + state.Title) + + existsFile, existsErr := os.Stat(possiblePath) + + if existsErr == nil && existsFile != nil { + best = &file{ + Path: possiblePath, + NameParts: parts, + } + return best + } + var maxScore int for _, f := range c.index { + var score int for _, part := range parts { for _, namePart := range f.NameParts { @@ -76,7 +103,10 @@ func (c *Client) findFile(query string) *file { } } } - return best + if strings.Contains(best.Path, state.Artist) && strings.Contains(best.Path, state.Album) && strings.Contains(best.Path, state.Title) && strings.Contains(best.Path, strconv.Itoa(state.TrackNumber)) { + return best + } + return nil } func createIndex(folder string) ([]*file, error) { @@ -93,8 +123,8 @@ func createIndex(folder string) ([]*file, error) { if d.IsDir() || !strings.HasSuffix(d.Name(), ".lrc") { return nil } - name := strings.TrimSuffix(d.Name(), ".lrc") - parts := splitString(name) + + parts := splitString(path) index = append(index, &file{ Path: path, @@ -134,3 +164,7 @@ func parseLrcLine(line string) lyrics.Line { Words: line[10:], } } + +func (c *Client) Name() string { + return "LOCAL" +} diff --git a/services/mopidy/mopidy.go b/services/mopidy/mopidy.go index 8ef50c1..bde3774 100644 --- a/services/mopidy/mopidy.go +++ b/services/mopidy/mopidy.go @@ -8,7 +8,7 @@ import ( "sptlrx/player" ) -func New(address string) *Client { +func New(address string) player.Player { return &Client{address: address} } @@ -71,12 +71,11 @@ func (c *Client) State() (*player.State, error) { artist += a.Name } - query := artist + " " + current.Result.Name - return &player.State{ ID: current.Result.URI, - Query: query, + Title: current.Result.Name, Position: position.Result, + Artist: artist, Playing: state.Result == "playing", }, err } diff --git a/services/mpd/mpd.go b/services/mpd/mpd.go index daa5dac..6243970 100644 --- a/services/mpd/mpd.go +++ b/services/mpd/mpd.go @@ -7,7 +7,7 @@ import ( "github.com/fhs/gompd/mpd" ) -func New(address, password string) *Client { +func New(address, password string) player.Player { return &Client{ address: address, password: password, @@ -66,16 +66,10 @@ func (c *Client) State() (*player.State, error) { artist = a } - var query string - if artist != "" { - query = artist + " " + title - } else { - query = title - } - return &player.State{ ID: status["songid"], - Query: query, + Artist: artist, + Title: title, Playing: status["state"] == "play", Position: int(elapsed) * 1000, }, nil diff --git a/services/mpris/mpris_unix.go b/services/mpris/mpris_unix.go index d8622c1..f36c425 100644 --- a/services/mpris/mpris_unix.go +++ b/services/mpris/mpris_unix.go @@ -3,14 +3,16 @@ package mpris import ( + "os" "sptlrx/player" + "strconv" "strings" "github.com/Pauloo27/go-mpris" "github.com/godbus/dbus/v5" ) -func New(players []string) (*Client, error) { +func New(players []string) (player.Player, error) { return &Client{players}, nil } @@ -52,6 +54,8 @@ func (c *Client) getPlayer() (*mpris.Player, error) { } func (c *Client) State() (*player.State, error) { + + os.Stderr.WriteString("MPRIS: Found Song" + "\n") p, err := c.getPlayer() if err != nil { return nil, err @@ -74,9 +78,28 @@ func (c *Client) State() (*player.State, error) { return nil, err } - var title string + var songPath string = "" + if p, ok := meta["xesam:url"].Value().(string); ok { + songPath = p + os.Stderr.WriteString("MPRIS: Path: " + songPath + "\n") + } + + var trackNumber int = 0 + if n, ok := meta["xesam:trackNumber"].Value().(int); ok { + trackNumber = n + os.Stderr.WriteString("MPRIS: TrackNum: " + strconv.Itoa(trackNumber) + "\n") + } + + var title string = "" if t, ok := meta["xesam:title"].Value().(string); ok { title = t + os.Stderr.WriteString("MPRIS: Title: " + title + "\n") + } + + var album string = "" + if al, ok := meta["xesam:album"].Value().(string); ok { + album = al + os.Stderr.WriteString("MPRIS: Album: " + album + "\n") } var artist string @@ -86,18 +109,16 @@ func (c *Client) State() (*player.State, error) { case []string: artist = strings.Join(a.([]string), " ") } - - var query string - if artist != "" { - query = artist + " " + title - } else { - query = title - } + os.Stderr.WriteString("MPRIS: Artist: " + artist + "\n") return &player.State{ - ID: query, // use query as id since mpris:trackid is broken - Query: query, - Position: int(position * 1000), // secs to ms - Playing: status == mpris.PlaybackPlaying, + ID: songPath, // use query as id since mpris:trackid is broken + TrackNumber: trackNumber, + Artist: artist, + Album: album, + Title: title, + SongPath: songPath, + Position: int(position * 1000), // secs to ms + Playing: status == mpris.PlaybackPlaying, }, err } diff --git a/services/spotify/spotify.go b/services/spotify/spotify.go index c4edc2b..4bd61b9 100644 --- a/services/spotify/spotify.go +++ b/services/spotify/spotify.go @@ -6,6 +6,7 @@ import ( "io" "net/http" "net/url" + "os" "sptlrx/lyrics" "sptlrx/player" "strings" @@ -21,7 +22,13 @@ const lyricsUrl = "https://spclient.wg.spotify.com/color-lyrics/v2/track/" const stateUrl = "https://api.spotify.com/v1/me/player/currently-playing" const searchUrl = "https://api.spotify.com/v1/search?" -func New(cookie string) (*Client, error) { +func NewProvider(cookie string) (lyrics.Provider, error) { + if cookie == "" { + return nil, ErrInvalidCookie + } + return &Client{cookie: cookie}, nil +} +func NewPlayer(cookie string) (player.Player, error) { if cookie == "" { return nil, ErrInvalidCookie } @@ -77,15 +84,32 @@ func (c *Client) State() (*player.State, error) { }, nil } -func (c *Client) Lyrics(id, query string) ([]lyrics.Line, error) { - if strings.HasPrefix(id, "spotify:") { - return c.lyrics(id[8:]) +func (c *Client) Lyrics(state player.State) ([]lyrics.Line, error) { + if strings.HasPrefix(state.ID, "spotify:") { + os.Stderr.WriteString("SPTFY: Found Lyrics" + "\n") + return c.lyrics(state.ID[8:]) } - id, err := c.search(query) + id, err := c.search(state.Artist + " " + state.Title) if err != nil { + os.Stderr.WriteString("SPTFY: Error Finding Lyrics" + "\n") + return nil, err + } + lys, err := c.lyrics(id) + if len(lys) > 0 && err != nil { + // var header []lyrics.Line + // header = append(header, lyrics.Line{ + // Time: 0, + // Words: "Loading from Spotify...", + // }) + // if lys[0].Time < 10 { + // lys[0].Time = 10 + // } + os.Stderr.WriteString("SPTFY: Found Lyrics" + "\n") + return lys, err + } else { + os.Stderr.WriteString("SPTFY: Empty Lyrics" + "\n") return nil, err } - return c.lyrics(id) } func (c *Client) search(query string) (string, error) { @@ -242,3 +266,7 @@ type searchBody struct { Total int `json:"total"` } `json:"tracks"` } + +func (c *Client) Name() string { + return "SPOTI" +}