diff --git a/app.go b/app.go index eaf9fb3..8479c5a 100644 --- a/app.go +++ b/app.go @@ -46,10 +46,14 @@ func (a *App) startup(ctx context.Context) { if err := backend.InitHistoryDB("SpotiFLAC"); err != nil { fmt.Printf("Failed to init history DB: %v\n", err) } + if err := backend.InitISRCCacheDB(); err != nil { + fmt.Printf("Failed to init ISRC cache DB: %v\n", err) + } } func (a *App) shutdown(ctx context.Context) { backend.CloseHistoryDB() + backend.CloseISRCCacheDB() } type SpotifyMetadataRequest struct { @@ -629,12 +633,8 @@ func (a *App) OpenFolder(path string) error { } func (a *App) OpenConfigFolder() error { - homeDir, err := os.UserHomeDir() + configDir, err := backend.EnsureAppDir() if err != nil { - return fmt.Errorf("failed to get home directory: %v", err) - } - configDir := filepath.Join(homeDir, ".spotiflac") - if err := os.MkdirAll(configDir, 0755); err != nil { return fmt.Errorf("failed to create config directory: %v", err) } return backend.OpenFolderInExplorer(configDir) diff --git a/backend/ffmpeg.go b/backend/ffmpeg.go index 5457c93..b9cd3e3 100644 --- a/backend/ffmpeg.go +++ b/backend/ffmpeg.go @@ -58,7 +58,7 @@ func ValidateExecutable(path string) error { return nil } -func GetFFmpegDir() (string, error) { +func GetAppDir() (string, error) { homeDir, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("failed to get home directory: %w", err) @@ -66,6 +66,23 @@ func GetFFmpegDir() (string, error) { return filepath.Join(homeDir, ".spotiflac"), nil } +func EnsureAppDir() (string, error) { + appDir, err := GetAppDir() + if err != nil { + return "", err + } + + if err := os.MkdirAll(appDir, 0o755); err != nil { + return "", fmt.Errorf("failed to create app directory: %w", err) + } + + return appDir, nil +} + +func GetFFmpegDir() (string, error) { + return EnsureAppDir() +} + func GetFFmpegPath() (string, error) { ffmpegDir, err := GetFFmpegDir() if err != nil { diff --git a/backend/history.go b/backend/history.go index 754db43..18804c0 100644 --- a/backend/history.go +++ b/backend/history.go @@ -3,7 +3,6 @@ package backend import ( "encoding/json" "fmt" - "os" "path/filepath" "sort" "time" @@ -35,13 +34,10 @@ const ( func InitHistoryDB(appName string) error { - appDir, err := GetFFmpegDir() + appDir, err := EnsureAppDir() if err != nil { return err } - if _, err := os.Stat(appDir); os.IsNotExist(err) { - os.MkdirAll(appDir, 0755) - } dbPath := filepath.Join(appDir, "history.db") db, err := bolt.Open(dbPath, 0600, &bolt.Options{Timeout: 1 * time.Second}) diff --git a/backend/isrc_cache.go b/backend/isrc_cache.go new file mode 100644 index 0000000..cbe6b34 --- /dev/null +++ b/backend/isrc_cache.go @@ -0,0 +1,137 @@ +package backend + +import ( + "encoding/json" + "fmt" + "path/filepath" + "strings" + "sync" + "time" + + bolt "go.etcd.io/bbolt" +) + +const ( + isrcCacheDBFile = "isrc_cache.db" + isrcCacheBucket = "SpotifyTrackISRC" +) + +type isrcCacheEntry struct { + TrackID string `json:"track_id"` + ISRC string `json:"isrc"` + UpdatedAt int64 `json:"updated_at"` +} + +var ( + isrcCacheDB *bolt.DB + isrcCacheDBMu sync.Mutex +) + +func InitISRCCacheDB() error { + isrcCacheDBMu.Lock() + defer isrcCacheDBMu.Unlock() + + if isrcCacheDB != nil { + return nil + } + + appDir, err := EnsureAppDir() + if err != nil { + return err + } + + dbPath := filepath.Join(appDir, isrcCacheDBFile) + db, err := bolt.Open(dbPath, 0o600, &bolt.Options{Timeout: 1 * time.Second}) + if err != nil { + return err + } + + if err := db.Update(func(tx *bolt.Tx) error { + _, err := tx.CreateBucketIfNotExists([]byte(isrcCacheBucket)) + return err + }); err != nil { + db.Close() + return err + } + + isrcCacheDB = db + return nil +} + +func CloseISRCCacheDB() { + isrcCacheDBMu.Lock() + defer isrcCacheDBMu.Unlock() + + if isrcCacheDB != nil { + _ = isrcCacheDB.Close() + isrcCacheDB = nil + } +} + +func GetCachedISRC(trackID string) (string, error) { + normalizedTrackID := strings.TrimSpace(trackID) + if normalizedTrackID == "" { + return "", nil + } + + if err := InitISRCCacheDB(); err != nil { + return "", err + } + + var cachedISRC string + err := isrcCacheDB.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(isrcCacheBucket)) + if bucket == nil { + return nil + } + + value := bucket.Get([]byte(normalizedTrackID)) + if len(value) == 0 { + return nil + } + + var entry isrcCacheEntry + if err := json.Unmarshal(value, &entry); err != nil { + return err + } + + cachedISRC = strings.ToUpper(strings.TrimSpace(entry.ISRC)) + return nil + }) + if err != nil { + return "", err + } + + return cachedISRC, nil +} + +func PutCachedISRC(trackID string, isrc string) error { + normalizedTrackID := strings.TrimSpace(trackID) + normalizedISRC := strings.ToUpper(strings.TrimSpace(isrc)) + if normalizedTrackID == "" || normalizedISRC == "" { + return nil + } + + if err := InitISRCCacheDB(); err != nil { + return err + } + + entry := isrcCacheEntry{ + TrackID: normalizedTrackID, + ISRC: normalizedISRC, + UpdatedAt: time.Now().Unix(), + } + + payload, err := json.Marshal(entry) + if err != nil { + return fmt.Errorf("failed to encode ISRC cache entry: %w", err) + } + + return isrcCacheDB.Update(func(tx *bolt.Tx) error { + bucket, err := tx.CreateBucketIfNotExists([]byte(isrcCacheBucket)) + if err != nil { + return err + } + return bucket.Put([]byte(normalizedTrackID), payload) + }) +} diff --git a/backend/isrc_finder.go b/backend/isrc_finder.go index 3b8f29e..ef72729 100644 --- a/backend/isrc_finder.go +++ b/backend/isrc_finder.go @@ -54,6 +54,14 @@ func (s *SongLinkClient) lookupSpotifyISRC(spotifyTrackID string) (string, error return "", err } + cachedISRC, err := GetCachedISRC(normalizedTrackID) + if err != nil { + fmt.Printf("Warning: failed to read ISRC cache: %v\n", err) + } else if cachedISRC != "" { + fmt.Printf("Found ISRC in cache: %s\n", cachedISRC) + return cachedISRC, nil + } + payload, err := fetchSpotifyTrackRawData(s.client, normalizedTrackID) if err != nil { return "", err @@ -65,6 +73,9 @@ func (s *SongLinkClient) lookupSpotifyISRC(spotifyTrackID string) (string, error } fmt.Printf("Found ISRC via Spotify metadata: %s\n", isrc) + if err := PutCachedISRC(normalizedTrackID, isrc); err != nil { + fmt.Printf("Warning: failed to write ISRC cache: %v\n", err) + } return isrc, nil } @@ -158,17 +169,12 @@ func saveSpotifyCachedToken(token *spotifyAnonymousToken) error { } func spotifyTokenCachePath() (string, error) { - cacheDir, err := os.UserCacheDir() - if err == nil && cacheDir != "" { - return filepath.Join(cacheDir, "SpotiFLAC", spotifyTokenCacheFile), nil - } - - wd, err := os.Getwd() + appDir, err := EnsureAppDir() if err != nil { - return "", fmt.Errorf("failed to determine working directory: %w", err) + return "", err } - return filepath.Join(wd, spotifyTokenCacheFile), nil + return filepath.Join(appDir, spotifyTokenCacheFile), nil } func spotifyTokenIsValid(token *spotifyAnonymousToken) bool {