Merge pull request #547 from dweymouth/lrclib-cache
Add cache for LrcLib lyrics
This commit is contained in:
@@ -48,6 +48,7 @@ type App struct {
|
||||
MPRISHandler *MPRISHandler
|
||||
WinSMTC *SMTC
|
||||
ipcServer ipc.IPCServer
|
||||
LrcLibFetcher *LrcLibFetcher
|
||||
|
||||
// UI callbacks to be set in main
|
||||
OnReactivate func()
|
||||
@@ -144,6 +145,9 @@ func StartupApp(appName, displayAppName, appVersion, appVersionTag, latestReleas
|
||||
a.ServerManager.SetPrefetchAlbumCoverCallback(func(coverID string) {
|
||||
_, _ = a.ImageManager.GetCoverThumbnail(coverID)
|
||||
})
|
||||
if a.Config.Application.EnableLrcLib {
|
||||
a.LrcLibFetcher = NewLrcLibFetcher(a.cacheDir)
|
||||
}
|
||||
|
||||
a.PlaybackManager.OnPlaying(func() {
|
||||
SetSystemSleepDisabled(true)
|
||||
|
||||
+94
-2
@@ -2,20 +2,69 @@ package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/20after4/configdir"
|
||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||
)
|
||||
|
||||
// FetchLrcLibLyrics is a static function to search and fetch lyrics from lrclib.net
|
||||
func FetchLrcLibLyrics(name, artist, album string, durationSecs int) (*mediaprovider.Lyrics, error) {
|
||||
const lrclibCacheFolder = "lrclib"
|
||||
|
||||
type LrcLibFetcher struct {
|
||||
cachePath string
|
||||
}
|
||||
|
||||
func NewLrcLibFetcher(baseCacheDir string) *LrcLibFetcher {
|
||||
cachePath := filepath.Join(baseCacheDir, lrclibCacheFolder)
|
||||
configdir.MakePath(cachePath)
|
||||
return &LrcLibFetcher{cachePath: cachePath}
|
||||
}
|
||||
|
||||
func (l *LrcLibFetcher) FetchLrcLibLyrics(name, artist, album string, durationSecs int) (*mediaprovider.Lyrics, error) {
|
||||
hash := makeTrackIdHash(name, artist, album, durationSecs)
|
||||
cacheFilePath := filepath.Join(l.cachePath, fmt.Sprintf("%s.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 := l.fetchFromServer(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
|
||||
}
|
||||
|
||||
func (l *LrcLibFetcher) fetchFromServer(name, artist, album string, durationSecs int) (*mediaprovider.Lyrics, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -105,3 +154,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, durationSecs int) string {
|
||||
hasher := md5.New()
|
||||
identifier := fmt.Sprintf("%s;%s;%s;%d", name, artist, album, durationSecs)
|
||||
hasher.Write([]byte(identifier))
|
||||
return hex.EncodeToString(hasher.Sum(nil))
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -80,7 +80,7 @@ type nowPlayingPageState struct {
|
||||
mp mediaprovider.MediaProvider
|
||||
canRate bool
|
||||
canShare bool
|
||||
lrcLib bool
|
||||
lrcFetch *backend.LrcLibFetcher
|
||||
}
|
||||
|
||||
func NewNowPlayingPage(
|
||||
@@ -93,10 +93,10 @@ func NewNowPlayingPage(
|
||||
mp mediaprovider.MediaProvider,
|
||||
canRate bool,
|
||||
canShare bool,
|
||||
lrcLibEnabled bool,
|
||||
lrcLibFetcher *backend.LrcLibFetcher,
|
||||
) *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, lrcFetch: lrcLibFetcher,
|
||||
}
|
||||
if page, ok := pool.Obtain(util.WidgetTypeNowPlayingPage).(*NowPlayingPage); ok && page != nil {
|
||||
page.nowPlayingPageState = state
|
||||
@@ -361,8 +361,8 @@ func (a *NowPlayingPage) fetchLyrics(ctx context.Context, song *mediaprovider.Tr
|
||||
log.Printf("Error fetching lyrics: %v", err)
|
||||
}
|
||||
}
|
||||
if lyrics == nil {
|
||||
lyrics, err = backend.FetchLrcLibLyrics(song.Title, song.ArtistNames[0], song.Album, song.Duration)
|
||||
if lyrics == nil && a.lrcFetch != nil {
|
||||
lyrics, err = a.lrcFetch.FetchLrcLibLyrics(song.Title, song.ArtistNames[0], song.Album, song.Duration)
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
}
|
||||
@@ -450,7 +450,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.lrcFetch)
|
||||
}
|
||||
|
||||
var _ CanShowPlayTime = (*NowPlayingPage)(nil)
|
||||
|
||||
@@ -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.LrcLibFetcher)
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user