Files
SpotiFLAC-Mobile/go_backend/duplicate.go
zarzet 81b0eede8c v3.3.5: Same as 3.3.1 but fixes crash issues caused by FFmpeg
Changes:
- Fix FFmpeg crash issues during M4A to MP3/Opus conversion
- Add format picker (MP3/Opus) when selecting Tidal Lossy 320kbps
- Fix Deezer album blank screen when opened from home
- LRC file generation now follows lyrics mode setting
- Version bump to 3.3.5 (build 70)
2026-02-01 20:12:00 +07:00

260 lines
5.4 KiB
Go

package gobackend
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
type ISRCIndex struct {
index map[string]string // ISRC (uppercase) -> file path
outputDir string
buildTime time.Time
mu sync.RWMutex
}
var (
isrcIndexCache = make(map[string]*ISRCIndex)
isrcIndexCacheMu sync.RWMutex
isrcBuildingMu sync.Map // Per-directory build lock to prevent concurrent builds
isrcIndexTTL = 5 * time.Minute
)
func GetISRCIndex(outputDir string) *ISRCIndex {
// Fast path: check cache first
isrcIndexCacheMu.RLock()
idx, exists := isrcIndexCache[outputDir]
isrcIndexCacheMu.RUnlock()
if exists && time.Since(idx.buildTime) < isrcIndexTTL {
return idx
}
// Slow path: need to build index
// Use per-directory mutex to prevent multiple goroutines from building simultaneously
buildLock, _ := isrcBuildingMu.LoadOrStore(outputDir, &sync.Mutex{})
mu := buildLock.(*sync.Mutex)
mu.Lock()
defer mu.Unlock()
// Double-check cache after acquiring lock (another goroutine may have built it)
isrcIndexCacheMu.RLock()
idx, exists = isrcIndexCache[outputDir]
isrcIndexCacheMu.RUnlock()
if exists && time.Since(idx.buildTime) < isrcIndexTTL {
return idx
}
return buildISRCIndex(outputDir)
}
func buildISRCIndex(outputDir string) *ISRCIndex {
idx := &ISRCIndex{
index: make(map[string]string),
outputDir: outputDir,
buildTime: time.Now(),
}
if outputDir == "" {
return idx
}
startTime := time.Now()
fileCount := 0
filepath.Walk(outputDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return nil
}
ext := strings.ToLower(filepath.Ext(path))
if ext != ".flac" {
return nil
}
metadata, err := ReadMetadata(path)
if err != nil || metadata.ISRC == "" {
return nil
}
idx.index[strings.ToUpper(metadata.ISRC)] = path
fileCount++
return nil
})
fmt.Printf("[ISRCIndex] Built index for %s: %d files in %v\n",
outputDir, fileCount, time.Since(startTime).Round(time.Millisecond))
isrcIndexCacheMu.Lock()
isrcIndexCache[outputDir] = idx
isrcIndexCacheMu.Unlock()
return idx
}
func (idx *ISRCIndex) lookup(isrc string) (string, bool) {
if isrc == "" {
return "", false
}
idx.mu.RLock()
defer idx.mu.RUnlock()
path, exists := idx.index[strings.ToUpper(isrc)]
return path, exists
}
func (idx *ISRCIndex) remove(isrc string) {
if isrc == "" {
return
}
idx.mu.Lock()
defer idx.mu.Unlock()
delete(idx.index, strings.ToUpper(isrc))
}
func (idx *ISRCIndex) Lookup(isrc string) (string, error) {
path, _ := idx.lookup(isrc)
return path, nil
}
func (idx *ISRCIndex) Add(isrc, filePath string) {
if isrc == "" || filePath == "" {
return
}
idx.mu.Lock()
defer idx.mu.Unlock()
idx.index[strings.ToUpper(isrc)] = filePath
}
func InvalidateISRCCache(outputDir string) {
isrcIndexCacheMu.Lock()
delete(isrcIndexCache, outputDir)
isrcIndexCacheMu.Unlock()
}
func checkISRCExistsInternal(outputDir, isrc string) (string, bool) {
if isrc == "" || outputDir == "" {
return "", false
}
idx := GetISRCIndex(outputDir)
filePath, exists := idx.lookup(isrc)
if !exists {
return "", false
}
if !CheckFileExists(filePath) {
// Stale index entry; remove it and return not found.
idx.remove(isrc)
return "", false
}
return filePath, true
}
func CheckISRCExists(outputDir, isrc string) (string, error) {
filepath, _ := checkISRCExistsInternal(outputDir, isrc)
return filepath, nil
}
func CheckFileExists(filePath string) bool {
info, err := os.Stat(filePath)
if err != nil {
return false
}
return !info.IsDir() && info.Size() > 0
}
type FileExistenceResult struct {
ISRC string `json:"isrc"`
Exists bool `json:"exists"`
FilePath string `json:"file_path,omitempty"`
TrackName string `json:"track_name,omitempty"`
ArtistName string `json:"artist_name,omitempty"`
}
func CheckFilesExistParallel(outputDir string, tracksJSON string) (string, error) {
var tracks []struct {
ISRC string `json:"isrc"`
TrackName string `json:"track_name"`
ArtistName string `json:"artist_name"`
}
if err := json.Unmarshal([]byte(tracksJSON), &tracks); err != nil {
return "", fmt.Errorf("failed to parse tracks JSON: %w", err)
}
results := make([]FileExistenceResult, len(tracks))
isrcIdx := GetISRCIndex(outputDir)
var wg sync.WaitGroup
for i, track := range tracks {
wg.Add(1)
go func(resultIdx int, t struct {
ISRC string `json:"isrc"`
TrackName string `json:"track_name"`
ArtistName string `json:"artist_name"`
}) {
defer wg.Done()
result := FileExistenceResult{
ISRC: t.ISRC,
TrackName: t.TrackName,
ArtistName: t.ArtistName,
Exists: false,
}
if t.ISRC != "" {
if filePath, exists := isrcIdx.lookup(t.ISRC); exists {
result.Exists = true
result.FilePath = filePath
}
}
results[resultIdx] = result
}(i, track)
}
wg.Wait()
resultJSON, err := json.Marshal(results)
if err != nil {
return "", fmt.Errorf("failed to marshal results: %w", err)
}
return string(resultJSON), nil
}
func PreBuildISRCIndex(outputDir string) error {
if outputDir == "" {
return fmt.Errorf("output directory is required")
}
buildISRCIndex(outputDir)
return nil
}
func AddToISRCIndex(outputDir, isrc, filePath string) {
if outputDir == "" || isrc == "" || filePath == "" {
return
}
isrcIndexCacheMu.RLock()
idx, exists := isrcIndexCache[outputDir]
isrcIndexCacheMu.RUnlock()
if exists {
idx.Add(isrc, filePath)
}
}