From 3dad29885051353bdb76867b06660fa842d4f532 Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Mon, 29 Jul 2024 17:47:55 +0200 Subject: [PATCH 1/6] add support for local lyric uri --- lyrics/lyrics.go | 4 ++- player/player.go | 9 +++++- pool/pool.go | 9 +++--- services/browser/browser.go | 6 ++-- services/hosted/hosted.go | 5 ++-- services/local/local.go | 55 +++++++++++++++++++++++++++--------- services/mopidy/mopidy.go | 6 ++-- services/mpd/mpd.go | 7 +++-- services/mpris/mpris_unix.go | 10 +++++-- services/spotify/spotify.go | 10 +++---- 10 files changed, 85 insertions(+), 36 deletions(-) diff --git a/lyrics/lyrics.go b/lyrics/lyrics.go index a1507fe..e5304c7 100644 --- a/lyrics/lyrics.go +++ b/lyrics/lyrics.go @@ -1,7 +1,9 @@ package lyrics +import "sptlrx/player" + type Provider interface { - Lyrics(id, query string) ([]Line, error) + Lyrics(track *player.TrackMetadata) ([]Line, error) } type Line struct { diff --git a/player/player.go b/player/player.go index a6ae10d..db98979 100644 --- a/player/player.go +++ b/player/player.go @@ -4,11 +4,18 @@ type Player interface { State() (*State, error) } -type State struct { +type TrackMetadata struct { // ID of the current track. ID string + // URI is the path to the local music file, if it exists. + // May be either absolute or relative to the local music directory (configured in "local" source). + Uri string // Query is a string that can be used to find lyrics. Query string +} + +type State struct { + Track TrackMetadata // 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..dbec1a5 100644 --- a/pool/pool.go +++ b/pool/pool.go @@ -44,10 +44,10 @@ func Listen( case newState := <-stateCh: lastUpdate = time.Now() - if newState.ID != state.ID { + if newState.Track.ID != state.Track.ID { changed = true - if newState.ID != "" { - newLines, err := provider.Lyrics(newState.ID, newState.Query) + if newState.Track.ID != "" { + newLines, err := provider.Lyrics(&newState.Track) if err != nil { state.Err = err } @@ -99,8 +99,7 @@ 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.Track = state.Track st.Playing = state.Playing st.Position = state.Position } diff --git a/services/browser/browser.go b/services/browser/browser.go index 693b0fd..f39f8e5 100644 --- a/services/browser/browser.go +++ b/services/browser/browser.go @@ -153,8 +153,10 @@ func (c *Client) State() (*player.State, error) { position += int(time.Since(c.updateTime).Milliseconds()) } return &player.State{ - ID: query, - Query: query, + Track: player.TrackMetadata{ + ID: query, + Query: query, + }, Position: position, Playing: c.state == playing, }, nil diff --git a/services/hosted/hosted.go b/services/hosted/hosted.go index 3d17b8b..ea0a7a2 100644 --- a/services/hosted/hosted.go +++ b/services/hosted/hosted.go @@ -6,6 +6,7 @@ import ( "net/http" "net/url" "sptlrx/lyrics" + "sptlrx/player" ) // Host your own: https://github.com/raitonoberu/lyricsapi @@ -20,8 +21,8 @@ type Client struct { host string } -func (c *Client) Lyrics(id, query string) ([]lyrics.Line, error) { - var url = fmt.Sprintf("https://%s/api/lyrics?name=%s", c.host, url.QueryEscape(query)) +func (c *Client) Lyrics(track *player.TrackMetadata) ([]lyrics.Line, error) { + var url = fmt.Sprintf("https://%s/api/lyrics?name=%s", c.host, url.QueryEscape(track.Query)) req, _ := http.NewRequest("GET", url, nil) resp, err := http.DefaultClient.Do(req) diff --git a/services/local/local.go b/services/local/local.go index 163028c..3f5866b 100644 --- a/services/local/local.go +++ b/services/local/local.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "sptlrx/lyrics" + "sptlrx/player" "strconv" "strings" ) @@ -26,25 +27,32 @@ type file struct { } func New(folder string) (*Client, error) { - index, err := createIndex(folder) + var expandedFolder string + if strings.HasPrefix(folder, "~/") { + dirname, _ := os.UserHomeDir() + expandedFolder = filepath.Join(dirname, folder[2:]) + } + + index, err := createIndex(expandedFolder) if err != nil { return nil, err } - return &Client{index: index}, nil + return &Client{folder: expandedFolder, index: index}, nil } // Client implements lyrics.Provider type Client struct { + folder string index []*file } -func (c *Client) Lyrics(id, query string) ([]lyrics.Line, error) { - f := c.findFile(query) - if f == nil { +func (c *Client) Lyrics(track *player.TrackMetadata) ([]lyrics.Line, error) { + f := c.findFile(track) + if f == "" { return nil, nil } - reader, err := os.Open(f.Path) + reader, err := os.Open(f) if err != nil { return nil, err } @@ -53,8 +61,29 @@ func (c *Client) Lyrics(id, query string) ([]lyrics.Line, error) { return parseLrcFile(reader), nil } -func (c *Client) findFile(query string) *file { - parts := splitString(query) +func (c *Client) findFile(track *player.TrackMetadata) string { + if track == nil { + return "" + } + + // If it is a local track, try for similarly named .lrc file first + if track.Uri != "" { + var absUri string + if filepath.IsAbs(track.Uri) { + // Uri is already absolute + absUri = track.Uri + } else { + // Uri is relative to local music directory + absUri = filepath.Join(c.folder, track.Uri) + } + absLyricsUri := strings.TrimSuffix(absUri, filepath.Ext(absUri)) + ".lrc" + if _, err := os.Stat(absLyricsUri); err == nil { + fmt.Print(absLyricsUri) + return absLyricsUri + } + } + + parts := splitString(track.Query) var best *file var maxScore int @@ -76,15 +105,13 @@ func (c *Client) findFile(query string) *file { } } } - return best + if best == nil { + return "" + } + return best.Path } func createIndex(folder string) ([]*file, error) { - if strings.HasPrefix(folder, "~/") { - dirname, _ := os.UserHomeDir() - folder = filepath.Join(dirname, folder[2:]) - } - index := []*file{} return index, filepath.WalkDir(folder, func(path string, d fs.DirEntry, err error) error { if d == nil { diff --git a/services/mopidy/mopidy.go b/services/mopidy/mopidy.go index 8ef50c1..cec1e41 100644 --- a/services/mopidy/mopidy.go +++ b/services/mopidy/mopidy.go @@ -74,8 +74,10 @@ func (c *Client) State() (*player.State, error) { query := artist + " " + current.Result.Name return &player.State{ - ID: current.Result.URI, - Query: query, + Track: player.TrackMetadata{ + ID: current.Result.URI, + Query: query, + }, Position: position.Result, Playing: state.Result == "playing", }, err diff --git a/services/mpd/mpd.go b/services/mpd/mpd.go index daa5dac..a97c221 100644 --- a/services/mpd/mpd.go +++ b/services/mpd/mpd.go @@ -74,8 +74,11 @@ func (c *Client) State() (*player.State, error) { } return &player.State{ - ID: status["songid"], - Query: query, + Track: player.TrackMetadata{ + ID: status["songid"], + Uri: current["file"], + Query: query, + }, Playing: status["state"] == "play", Position: int(elapsed) * 1000, }, nil diff --git a/services/mpris/mpris_unix.go b/services/mpris/mpris_unix.go index f34fb6b..34d9487 100644 --- a/services/mpris/mpris_unix.go +++ b/services/mpris/mpris_unix.go @@ -81,11 +81,14 @@ func (c *Client) State() (*player.State, error) { title = t } + var uri string // In case the player uses the file name with extension as title if u, ok := meta["xesam:url"].Value().(string); ok { u, err := url.Parse(u) if err == nil { ext := filepath.Ext(u.Path) + uri = u.Path + // some players use filename as title when tag is absent => trim extension from title title = strings.TrimSuffix(title, ext) } } @@ -106,8 +109,11 @@ func (c *Client) State() (*player.State, error) { } return &player.State{ - ID: query, // use query as id since mpris:trackid is broken - Query: query, + Track: player.TrackMetadata{ + ID: query, // use query as id since mpris:trackid is broken + Uri: uri, + Query: query, + }, 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 faf0221..63a7a58 100644 --- a/services/spotify/spotify.go +++ b/services/spotify/spotify.go @@ -33,21 +33,21 @@ func (c *Client) State() (*player.State, error) { } return &player.State{ - ID: "spotify:" + result.Item.ID, + Track: player.TrackMetadata{ID: "spotify:" + result.Item.ID}, Position: result.Progress, Playing: result.Playing, }, nil } -func (c *Client) Lyrics(id, query string) ([]lyrics.Line, error) { +func (c *Client) Lyrics(track *player.TrackMetadata) ([]lyrics.Line, error) { var ( result *lyricsapi.LyricsResult err error ) - if strings.HasPrefix(id, "spotify:") { - result, err = c.api.GetByID(id[8:]) + if strings.HasPrefix(track.ID, "spotify:") { + result, err = c.api.GetByID(track.ID[8:]) } else { - result, err = c.api.GetByName(query) + result, err = c.api.GetByName(track.Query) } if err != nil { From 974de925037ec39ac8502b5eeb57e0d77c34428d Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Mon, 29 Jul 2024 18:00:23 +0200 Subject: [PATCH 2/6] README --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b96ff54..2b5fd3c 100644 --- a/README.md +++ b/README.md @@ -229,7 +229,9 @@ local: folder: "" ``` -If you want to use your local collection of `.lrc` files to display lyrics, specify the folder to scan. The application will use files with the most similar name. All other lyrics sources will be disabled. +If you want to use your local collection of `.lrc` files to display lyrics, specify the folder to scan. The application will use files with the most similar name. When used in conjunction with a local player, it will first look for a file with the same path as the music file, with the extension replaced by `.lrc`. + +All other lyrics sources will be disabled. ## Information From a3818c3e5c165e0b2108644e82962b01af53c31f Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Mon, 29 Jul 2024 18:04:16 +0200 Subject: [PATCH 3/6] dropped debug print --- services/local/local.go | 1 - 1 file changed, 1 deletion(-) diff --git a/services/local/local.go b/services/local/local.go index 3f5866b..aaac93b 100644 --- a/services/local/local.go +++ b/services/local/local.go @@ -78,7 +78,6 @@ func (c *Client) findFile(track *player.TrackMetadata) string { } absLyricsUri := strings.TrimSuffix(absUri, filepath.Ext(absUri)) + ".lrc" if _, err := os.Stat(absLyricsUri); err == nil { - fmt.Print(absLyricsUri) return absLyricsUri } } From ca05a62c0b06089be1775466661aa4ddabf2ac14 Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Sun, 25 Aug 2024 13:37:55 +0200 Subject: [PATCH 4/6] support local lyric source without folder --- README.md | 10 +++++++++- cmd/root.go | 3 ++- config/config.go | 1 + services/local/local.go | 16 ++++++++++++---- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2b5fd3c..c98596d 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,9 @@ browser: ### Local lyrics source ### local: + # Enable the local lyrics source. + # For backwards compatibility reasons setting the folder also enables this source. + enabled: false # Folder for scanning .lrc files. Example: "~/Music". folder: "" ``` @@ -226,10 +229,15 @@ You need to install a [browser extension](https://wnp.keifufu.dev/extension/gett ```yaml # config.yaml local: + enabled: true folder: "" ``` -If you want to use your local collection of `.lrc` files to display lyrics, specify the folder to scan. The application will use files with the most similar name. When used in conjunction with a local player, it will first look for a file with the same path as the music file, with the extension replaced by `.lrc`. +Display lyrics from local `.lrc` files. + +By default, the application will look for a file that is a sibling of a local music file (e.g. local player via mpdris), i.e. with the same path, with the extension replaced by `.lrc`. + +If the `folder` config option is set, it will additionally search for files within that folder. If the player provides a relative path to the music file (e.g. mpd), an exact match is attempted first as described above. If that fails, a best-effort search will be performed, returning a `.lrc` file in the folder (can be nested) with the most similar name. All other lyrics sources will be disabled. diff --git a/cmd/root.go b/cmd/root.go index b05a43a..7dc342a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -141,7 +141,8 @@ func loadPlayer(conf *config.Config) (player.Player, error) { } func loadProvider(conf *config.Config, player player.Player) (lyrics.Provider, error) { - if conf.Local.Folder != "" { + // For backwards compatibility reasons, this is auto-enabled when Folder is set + if conf.Local.Enabled || conf.Local.Folder != "" { return local.New(conf.Local.Folder) } if conf.Cookie == "" { diff --git a/config/config.go b/config/config.go index e67cebd..0925588 100644 --- a/config/config.go +++ b/config/config.go @@ -69,6 +69,7 @@ type Config struct { } `yaml:"browser"` Local struct { + Enabled bool `default: "false" yaml:"enabled"` Folder string `yaml:"folder"` } `yaml:"local"` } diff --git a/services/local/local.go b/services/local/local.go index aaac93b..0cfdf7c 100644 --- a/services/local/local.go +++ b/services/local/local.go @@ -72,13 +72,18 @@ func (c *Client) findFile(track *player.TrackMetadata) string { if filepath.IsAbs(track.Uri) { // Uri is already absolute absUri = track.Uri - } else { + } else if c.folder != "" { // Uri is relative to local music directory absUri = filepath.Join(c.folder, track.Uri) + } else { + // Can not handle relative uri without folder configured + absUri = "" } - absLyricsUri := strings.TrimSuffix(absUri, filepath.Ext(absUri)) + ".lrc" - if _, err := os.Stat(absLyricsUri); err == nil { - return absLyricsUri + if absUri != "" { + absLyricsUri := strings.TrimSuffix(absUri, filepath.Ext(absUri)) + ".lrc" + if _, err := os.Stat(absLyricsUri); err == nil { + return absLyricsUri + } } } @@ -112,6 +117,9 @@ func (c *Client) findFile(track *player.TrackMetadata) string { func createIndex(folder string) ([]*file, error) { index := []*file{} + if folder == "" { + return index, nil + } return index, filepath.WalkDir(folder, func(path string, d fs.DirEntry, err error) error { if d == nil { return fmt.Errorf("invalid path: %s", path) From cec12705df77dab1f04c4f51fcbbaee06b985f74 Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Sun, 25 Aug 2024 14:10:14 +0200 Subject: [PATCH 5/6] store any uri instead of only local ones --- README.md | 2 +- player/player.go | 7 +++--- services/local/local.go | 49 +++++++++++++++++++++++------------- services/mpd/mpd.go | 9 ++++++- services/mpris/mpris_unix.go | 6 ++--- 5 files changed, 47 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index c98596d..e268364 100644 --- a/README.md +++ b/README.md @@ -235,7 +235,7 @@ local: Display lyrics from local `.lrc` files. -By default, the application will look for a file that is a sibling of a local music file (e.g. local player via mpdris), i.e. with the same path, with the extension replaced by `.lrc`. +By default, the application will look for a file that is a sibling of a local music file (e.g. local player via mpris), i.e. with the same path, with the extension replaced by `.lrc`. If the `folder` config option is set, it will additionally search for files within that folder. If the player provides a relative path to the music file (e.g. mpd), an exact match is attempted first as described above. If that fails, a best-effort search will be performed, returning a `.lrc` file in the folder (can be nested) with the most similar name. diff --git a/player/player.go b/player/player.go index db98979..c7510da 100644 --- a/player/player.go +++ b/player/player.go @@ -1,5 +1,7 @@ package player +import "net/url" + type Player interface { State() (*State, error) } @@ -7,9 +9,8 @@ type Player interface { type TrackMetadata struct { // ID of the current track. ID string - // URI is the path to the local music file, if it exists. - // May be either absolute or relative to the local music directory (configured in "local" source). - Uri string + // URI to music file, if it is known. May be a (local) relative path. + Uri *url.URL // Query is a string that can be used to find lyrics. Query string } diff --git a/services/local/local.go b/services/local/local.go index 0cfdf7c..db94c98 100644 --- a/services/local/local.go +++ b/services/local/local.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "io/fs" + "net/url" "os" "path/filepath" "sptlrx/lyrics" @@ -67,26 +68,12 @@ func (c *Client) findFile(track *player.TrackMetadata) string { } // If it is a local track, try for similarly named .lrc file first - if track.Uri != "" { - var absUri string - if filepath.IsAbs(track.Uri) { - // Uri is already absolute - absUri = track.Uri - } else if c.folder != "" { - // Uri is relative to local music directory - absUri = filepath.Join(c.folder, track.Uri) - } else { - // Can not handle relative uri without folder configured - absUri = "" - } - if absUri != "" { - absLyricsUri := strings.TrimSuffix(absUri, filepath.Ext(absUri)) + ".lrc" - if _, err := os.Stat(absLyricsUri); err == nil { - return absLyricsUri - } - } + var exactMatch string = c.fileByLocalUri(track.Uri) + if exactMatch != "" { + return exactMatch } + // Fall back to best-effort search parts := splitString(track.Query) var best *file @@ -115,6 +102,32 @@ func (c *Client) findFile(track *player.TrackMetadata) string { return best.Path } +func (c *Client) fileByLocalUri(uri *url.URL) string { + if uri == nil { + return "" + } + if uri.Scheme != "file" && uri.Scheme != "" { + return "" + } + var absUri string + if filepath.IsAbs(uri.Path) { + // uri is already absolute + absUri = uri.Path + } else if c.folder != "" { + // Uri is relative to local music directory + absUri = filepath.Join(c.folder, uri.Path) + } else { + // Can not handle relative uri without folder configured + return "" + } + absLyricsUri := strings.TrimSuffix(absUri, filepath.Ext(absUri)) + ".lrc" + _, err := os.Stat(absLyricsUri) + if err != nil { + return "" + } + return absLyricsUri +} + func createIndex(folder string) ([]*file, error) { index := []*file{} if folder == "" { diff --git a/services/mpd/mpd.go b/services/mpd/mpd.go index a97c221..aa65daa 100644 --- a/services/mpd/mpd.go +++ b/services/mpd/mpd.go @@ -1,6 +1,7 @@ package mpd import ( + "net/url" "sptlrx/player" "strconv" @@ -73,10 +74,16 @@ func (c *Client) State() (*player.State, error) { query = title } + var uri *url.URL + u, err := url.Parse(current["file"]) + if err == nil && u.Path != "" { + uri = u + } + return &player.State{ Track: player.TrackMetadata{ ID: status["songid"], - Uri: current["file"], + Uri: uri, Query: query, }, Playing: status["state"] == "play", diff --git a/services/mpris/mpris_unix.go b/services/mpris/mpris_unix.go index 34d9487..082f6ae 100644 --- a/services/mpris/mpris_unix.go +++ b/services/mpris/mpris_unix.go @@ -81,13 +81,13 @@ func (c *Client) State() (*player.State, error) { title = t } - var uri string + var uri *url.URL // In case the player uses the file name with extension as title if u, ok := meta["xesam:url"].Value().(string); ok { u, err := url.Parse(u) - if err == nil { + if err == nil && u.Path != "" { ext := filepath.Ext(u.Path) - uri = u.Path + uri = u // some players use filename as title when tag is absent => trim extension from title title = strings.TrimSuffix(title, ext) } From 867a183f64f25054c5a0461cf7f8d251b1531fe4 Mon Sep 17 00:00:00 2001 From: Sander Van Balen Date: Tue, 27 Aug 2024 18:50:00 +0200 Subject: [PATCH 6/6] bugfix for absolute path --- services/local/local.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/local/local.go b/services/local/local.go index db94c98..6770653 100644 --- a/services/local/local.go +++ b/services/local/local.go @@ -32,6 +32,8 @@ func New(folder string) (*Client, error) { if strings.HasPrefix(folder, "~/") { dirname, _ := os.UserHomeDir() expandedFolder = filepath.Join(dirname, folder[2:]) + } else { + expandedFolder = folder } index, err := createIndex(expandedFolder)