151 lines
2.9 KiB
Go
151 lines
2.9 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// NoLyricsCache tracks songs that previously had no results.
|
|
type NoLyricsCache struct {
|
|
Tracks map[string]time.Time
|
|
mutex sync.RWMutex
|
|
}
|
|
|
|
func loadCache(cacheFile string) (*NoLyricsCache, error) {
|
|
cache := &NoLyricsCache{Tracks: make(map[string]time.Time)}
|
|
|
|
if _, err := os.Stat(cacheFile); os.IsNotExist(err) {
|
|
return cache, nil
|
|
}
|
|
|
|
data, err := os.ReadFile(cacheFile)
|
|
if err != nil {
|
|
return cache, err
|
|
}
|
|
|
|
if len(data) == 0 {
|
|
return cache, nil
|
|
}
|
|
|
|
err = json.Unmarshal(data, &cache.Tracks)
|
|
if cache.Tracks == nil {
|
|
cache.Tracks = make(map[string]time.Time)
|
|
}
|
|
|
|
return cache, err
|
|
}
|
|
|
|
func (c *NoLyricsCache) SaveCache(cacheFile string, ttl time.Duration) error {
|
|
cacheSnapshot := c.prunedCopy(ttl)
|
|
|
|
data, err := json.MarshalIndent(cacheSnapshot, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return writeFileAtomically(cacheFile, data, 0644)
|
|
}
|
|
|
|
func (c *NoLyricsCache) IsTrackedAsNoLyrics(trackID string, ttl time.Duration) bool {
|
|
c.mutex.RLock()
|
|
timestamp, exists := c.Tracks[trackID]
|
|
c.mutex.RUnlock()
|
|
|
|
if !exists {
|
|
return false
|
|
}
|
|
|
|
if isCacheEntryExpired(timestamp, ttl) {
|
|
c.mutex.Lock()
|
|
if ts, ok := c.Tracks[trackID]; ok && isCacheEntryExpired(ts, ttl) {
|
|
delete(c.Tracks, trackID)
|
|
}
|
|
c.mutex.Unlock()
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (c *NoLyricsCache) AddNoLyrics(trackID string) {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
c.Tracks[trackID] = time.Now()
|
|
}
|
|
|
|
func (c *NoLyricsCache) prunedCopy(ttl time.Duration) map[string]time.Time {
|
|
c.mutex.Lock()
|
|
defer c.mutex.Unlock()
|
|
|
|
for trackID, timestamp := range c.Tracks {
|
|
if isCacheEntryExpired(timestamp, ttl) {
|
|
delete(c.Tracks, trackID)
|
|
}
|
|
}
|
|
|
|
cacheSnapshot := make(map[string]time.Time, len(c.Tracks))
|
|
for trackID, timestamp := range c.Tracks {
|
|
cacheSnapshot[trackID] = timestamp
|
|
}
|
|
|
|
return cacheSnapshot
|
|
}
|
|
|
|
func createTrackIdentifier(metadata Metadata) string {
|
|
return fmt.Sprintf("%s-%s-%s",
|
|
strings.ToLower(strings.TrimSpace(metadata.ArtistName)),
|
|
strings.ToLower(strings.TrimSpace(metadata.AlbumName)),
|
|
strings.ToLower(strings.TrimSpace(metadata.TrackName)))
|
|
}
|
|
|
|
func isCacheEntryExpired(timestamp time.Time, ttl time.Duration) bool {
|
|
if ttl <= 0 {
|
|
return false
|
|
}
|
|
|
|
return time.Since(timestamp) > ttl
|
|
}
|
|
|
|
func writeFileAtomically(path string, data []byte, perm os.FileMode) error {
|
|
dir := filepath.Dir(path)
|
|
base := filepath.Base(path)
|
|
|
|
tempFile, err := os.CreateTemp(dir, base+".tmp-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tempPath := tempFile.Name()
|
|
cleanup := func() {
|
|
tempFile.Close()
|
|
os.Remove(tempPath)
|
|
}
|
|
|
|
if _, err := tempFile.Write(data); err != nil {
|
|
cleanup()
|
|
return err
|
|
}
|
|
|
|
if err := tempFile.Chmod(perm); err != nil {
|
|
cleanup()
|
|
return err
|
|
}
|
|
|
|
if err := tempFile.Close(); err != nil {
|
|
os.Remove(tempPath)
|
|
return err
|
|
}
|
|
|
|
if err := os.Rename(tempPath, path); err != nil {
|
|
os.Remove(tempPath)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|