Merge pull request #544 from JojiiOfficial/main
Add caching for lrclib lyrics
This commit is contained in:
+3
-3
@@ -57,7 +57,7 @@ type App struct {
|
|||||||
displayAppName string
|
displayAppName string
|
||||||
appVersionTag string
|
appVersionTag string
|
||||||
configDir string
|
configDir string
|
||||||
cacheDir string
|
CacheDir string
|
||||||
portableMode bool
|
portableMode bool
|
||||||
|
|
||||||
isFirstLaunch bool // set by config file reader
|
isFirstLaunch bool // set by config file reader
|
||||||
@@ -103,7 +103,7 @@ func StartupApp(appName, displayAppName, appVersion, appVersionTag, latestReleas
|
|||||||
displayAppName: displayAppName,
|
displayAppName: displayAppName,
|
||||||
appVersionTag: appVersionTag,
|
appVersionTag: appVersionTag,
|
||||||
configDir: confDir,
|
configDir: confDir,
|
||||||
cacheDir: cacheDir,
|
CacheDir: cacheDir,
|
||||||
portableMode: portableMode,
|
portableMode: portableMode,
|
||||||
}
|
}
|
||||||
a.bgrndCtx, a.cancel = context.WithCancel(context.Background())
|
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 {
|
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)
|
log.Printf("Deleting server cache dir: %s", path)
|
||||||
return os.RemoveAll(path)
|
return os.RemoveAll(path)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,60 @@ package backend
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/20after4/configdir"
|
||||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const CACHE_LYRICS_FOLDER = "lyrics"
|
||||||
|
|
||||||
|
func FetchLrcLibLyricsCached(name, artist, album string, durationSecs int, cacheDir string) (*mediaprovider.Lyrics, error) {
|
||||||
|
hash := makeTrackIdHash(name, artist, album, durationSecs)
|
||||||
|
cachePath := filepath.Join(cacheDir, CACHE_LYRICS_FOLDER)
|
||||||
|
configdir.MakePath(cachePath)
|
||||||
|
cacheFilePath := filepath.Join(cachePath, 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
|
// FetchLrcLibLyrics is a static function to search and fetch lyrics from lrclib.net
|
||||||
func FetchLrcLibLyrics(name, artist, album string, durationSecs int) (*mediaprovider.Lyrics, error) {
|
func FetchLrcLibLyrics(name, artist, album string, durationSecs int) (*mediaprovider.Lyrics, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
@@ -105,3 +147,46 @@ type lrcLibResponse struct {
|
|||||||
PlainLyrics string `json:"plainLyrics"`
|
PlainLyrics string `json:"plainLyrics"`
|
||||||
SyncedLyrics string `json:"syncedLyrics"`
|
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 {
|
type Lyrics struct {
|
||||||
Title string
|
Title string `json:"title"`
|
||||||
Artist string
|
Artist string `json:"artist"`
|
||||||
Synced bool
|
Synced bool `json:"synced"`
|
||||||
Lines []LyricLine
|
Lines []LyricLine `json:"lines"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LyricLine struct {
|
type LyricLine struct {
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ type nowPlayingPageState struct {
|
|||||||
canRate bool
|
canRate bool
|
||||||
canShare bool
|
canShare bool
|
||||||
lrcLib bool
|
lrcLib bool
|
||||||
|
cacheDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNowPlayingPage(
|
func NewNowPlayingPage(
|
||||||
@@ -94,9 +95,10 @@ func NewNowPlayingPage(
|
|||||||
canRate bool,
|
canRate bool,
|
||||||
canShare bool,
|
canShare bool,
|
||||||
lrcLibEnabled bool,
|
lrcLibEnabled bool,
|
||||||
|
cacheDir string,
|
||||||
) *NowPlayingPage {
|
) *NowPlayingPage {
|
||||||
state := nowPlayingPageState{
|
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 {
|
if page, ok := pool.Obtain(util.WidgetTypeNowPlayingPage).(*NowPlayingPage); ok && page != nil {
|
||||||
page.nowPlayingPageState = state
|
page.nowPlayingPageState = state
|
||||||
@@ -362,7 +364,7 @@ func (a *NowPlayingPage) fetchLyrics(ctx context.Context, song *mediaprovider.Tr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if lyrics == nil {
|
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 {
|
if err != nil {
|
||||||
log.Println(err.Error())
|
log.Println(err.Error())
|
||||||
}
|
}
|
||||||
@@ -450,7 +452,7 @@ func (a *NowPlayingPage) Reload() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *nowPlayingPageState) Restore() Page {
|
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)
|
var _ CanShowPlayTime = (*NowPlayingPage)(nil)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func (r Router) CreatePage(rte controller.Route) Page {
|
|||||||
case controller.Genres:
|
case controller.Genres:
|
||||||
return NewGenresPage(r.Controller, r.App.ServerManager.Server)
|
return NewGenresPage(r.Controller, r.App.ServerManager.Server)
|
||||||
case controller.NowPlaying:
|
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:
|
case controller.Playlist:
|
||||||
return NewPlaylistPage(rte.Arg, &r.App.Config.PlaylistPage, r.widgetPool, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager)
|
return NewPlaylistPage(rte.Arg, &r.App.Config.PlaylistPage, r.widgetPool, r.Controller, r.App.ServerManager, r.App.PlaybackManager, r.App.ImageManager)
|
||||||
case controller.Playlists:
|
case controller.Playlists:
|
||||||
|
|||||||
Reference in New Issue
Block a user