diff --git a/backend/app.go b/backend/app.go index 5a97309..13d413b 100644 --- a/backend/app.go +++ b/backend/app.go @@ -57,7 +57,7 @@ type App struct { displayAppName string appVersionTag string configDir string - cacheDir string + CacheDir string portableMode bool isFirstLaunch bool // set by config file reader @@ -103,7 +103,7 @@ func StartupApp(appName, displayAppName, appVersion, appVersionTag, latestReleas displayAppName: displayAppName, appVersionTag: appVersionTag, configDir: confDir, - cacheDir: cacheDir, + CacheDir: cacheDir, portableMode: portableMode, } a.bgrndCtx, a.cancel = context.WithCancel(context.Background()) @@ -411,7 +411,7 @@ func (a *App) LoginToDefaultServer(string) error { } func (a *App) DeleteServerCacheDir(serverID uuid.UUID) error { - path := path.Join(a.cacheDir, serverID.String()) + path := path.Join(a.CacheDir, serverID.String()) log.Printf("Deleting server cache dir: %s", path) return os.RemoveAll(path) } diff --git a/backend/lrclib.go b/backend/lrclib.go index 6277af4..4c652b9 100644 --- a/backend/lrclib.go +++ b/backend/lrclib.go @@ -2,10 +2,15 @@ package backend import ( "context" + "crypto/md5" + "encoding/hex" "encoding/json" "errors" "fmt" + "log" "net/http" + "os" + "path/filepath" "regexp" "strconv" "strings" @@ -14,6 +19,38 @@ import ( "github.com/dweymouth/supersonic/backend/mediaprovider" ) +func FetchLrcLibLyricsCached(name, artist, album string, durationSecs int, cacheDir string) (*mediaprovider.Lyrics, error) { + hash := makeTrackIdHash(name, artist, album) + cacheFilePath := filepath.Join(cacheDir, fmt.Sprintf("%s_lyrics.txt", hash)) + + // File is cached. Try to use it + if _, err := os.Stat(cacheFilePath); err == nil { + lyrics, err := readCachedLyrics(cacheFilePath) + if err == nil { + return lyrics, nil + } + + // On an error, remove the file. + if !os.IsNotExist(err) { + os.Remove(cacheFilePath) + } + } + + // Fetch the lyrics + lyrics, err := FetchLrcLibLyrics(name, artist, album, durationSecs) + if err != nil { + return nil, err + } + + // Try to write it into cache + err = writeCachedLyrics(cacheFilePath, lyrics) + if err != nil { + log.Printf("Failed to serialize fetched lyrics: %s", err) + } + + return lyrics, nil +} + // FetchLrcLibLyrics is a static function to search and fetch lyrics from lrclib.net func FetchLrcLibLyrics(name, artist, album string, durationSecs int) (*mediaprovider.Lyrics, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) @@ -105,3 +142,46 @@ type lrcLibResponse struct { PlainLyrics string `json:"plainLyrics"` SyncedLyrics string `json:"syncedLyrics"` } + +// Write lyrics to the given file. +func writeCachedLyrics(cacheFile string, lyrics *mediaprovider.Lyrics) error { + serialized, err := json.Marshal(lyrics) + if err != nil { + return err + } + + f, err := os.Create(cacheFile) + if err != nil { + return nil + } + defer f.Close() + + f.Write(serialized) + + return nil +} + +// Read lyrics from the given cache file. +func readCachedLyrics(cacheFile string) (*mediaprovider.Lyrics, error) { + cachedBytes, err := os.ReadFile(cacheFile) + if err != nil { + return nil, err + } + + var lyrics mediaprovider.Lyrics + + err = json.Unmarshal(cachedBytes, &lyrics) + if err != nil { + return nil, err + } + + return &lyrics, nil +} + +// Create a "unique" hash for a song to identify it. +func makeTrackIdHash(name, artist, album string) string { + hasher := md5.New() + identifier := fmt.Sprintf("%s;%s;%s", name, artist, album) + hasher.Write([]byte(identifier)) + return hex.EncodeToString(hasher.Sum(nil)) +} diff --git a/backend/mediaprovider/model.go b/backend/mediaprovider/model.go index b2ec9d7..4c65c80 100644 --- a/backend/mediaprovider/model.go +++ b/backend/mediaprovider/model.go @@ -150,10 +150,10 @@ type PlaylistWithTracks struct { } type Lyrics struct { - Title string - Artist string - Synced bool - Lines []LyricLine + Title string `json:"title"` + Artist string `json:"artist"` + Synced bool `json:"synced"` + Lines []LyricLine `json:"lines"` } type LyricLine struct { diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index 0c6eab7..8813c46 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.go @@ -81,6 +81,7 @@ type nowPlayingPageState struct { canRate bool canShare bool lrcLib bool + cacheDir string } func NewNowPlayingPage( @@ -94,9 +95,10 @@ func NewNowPlayingPage( canRate bool, canShare bool, lrcLibEnabled bool, + cacheDir string, ) *NowPlayingPage { state := nowPlayingPageState{ - conf: conf, contr: contr, pool: pool, sm: sm, im: im, pm: pm, mp: mp, canRate: canRate, canShare: canShare, lrcLib: lrcLibEnabled, + conf: conf, contr: contr, pool: pool, sm: sm, im: im, pm: pm, mp: mp, canRate: canRate, canShare: canShare, lrcLib: lrcLibEnabled, cacheDir: cacheDir, } if page, ok := pool.Obtain(util.WidgetTypeNowPlayingPage).(*NowPlayingPage); ok && page != nil { page.nowPlayingPageState = state @@ -362,7 +364,7 @@ func (a *NowPlayingPage) fetchLyrics(ctx context.Context, song *mediaprovider.Tr } } if lyrics == nil { - lyrics, err = backend.FetchLrcLibLyrics(song.Title, song.ArtistNames[0], song.Album, song.Duration) + lyrics, err = backend.FetchLrcLibLyricsCached(song.Title, song.ArtistNames[0], song.Album, song.Duration, a.cacheDir) if err != nil { log.Println(err.Error()) } @@ -450,7 +452,7 @@ func (a *NowPlayingPage) Reload() { } func (s *nowPlayingPageState) Restore() Page { - return NewNowPlayingPage(s.conf, s.contr, s.pool, s.sm, s.im, s.pm, s.mp, s.canRate, s.canShare, s.lrcLib) + return NewNowPlayingPage(s.conf, s.contr, s.pool, s.sm, s.im, s.pm, s.mp, s.canRate, s.canShare, s.lrcLib, s.cacheDir) } var _ CanShowPlayTime = (*NowPlayingPage)(nil) diff --git a/ui/browsing/router.go b/ui/browsing/router.go index cead13c..cfd3300 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -48,7 +48,7 @@ func (r Router) CreatePage(rte controller.Route) Page { case controller.Genres: return NewGenresPage(r.Controller, r.App.ServerManager.Server) case controller.NowPlaying: - return NewNowPlayingPage(&r.App.Config.NowPlayingConfig, r.Controller, r.widgetPool, r.App.ServerManager, r.App.ImageManager, r.App.PlaybackManager, r.App.ServerManager.Server, canRate, canShare, r.App.Config.Application.EnableLrcLib) + return NewNowPlayingPage(&r.App.Config.NowPlayingConfig, r.Controller, r.widgetPool, r.App.ServerManager, r.App.ImageManager, r.App.PlaybackManager, r.App.ServerManager.Server, canRate, canShare, r.App.Config.Application.EnableLrcLib, r.App.CacheDir) case controller.Playlist: return NewPlaylistPage(rte.Arg, &r.App.Config.PlaylistPage, r.widgetPool, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager) case controller.Playlists: