mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-24 02:20:59 +02:00
feat(library): ISRC duplicate detection beyond FLAC with review sheet
The download-time ISRC index now also parses mp3, m4a, ogg, and opus tags through the existing native readers instead of skipping everything but .flac. Library settings gain a Review duplicates sheet that groups history and local-library rows sharing an ISRC via SQL over the attached databases (no filesystem walk, SAF-safe), shows quality badges, and offers keep-best and per-copy delete with confirmation.
This commit is contained in:
@@ -159,6 +159,40 @@ func writeTestFlacWithISRC(t *testing.T, path, isrc string) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestISRCIndexCoversNonFlacFormats(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
flacPath := filepath.Join(dir, "a.flac")
|
||||
mp3Path := filepath.Join(dir, "b.mp3")
|
||||
writeTestFlacWithISRC(t, flacPath, "USAA00000011")
|
||||
mp3Data := buildID3v23Tag(
|
||||
id3TextFrame("TIT2", "Song"),
|
||||
id3TextFrame("TSRC", "usbb00000022"),
|
||||
)
|
||||
if err := os.WriteFile(mp3Path, mp3Data, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Unsupported/untagged formats must stay invisible to the index.
|
||||
if err := os.WriteFile(filepath.Join(dir, "c.wav"), []byte("RIFF"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer InvalidateISRCCache(dir)
|
||||
|
||||
if got := readFileISRC(mp3Path); got != "usbb00000022" {
|
||||
t.Fatalf("readFileISRC mp3 = %q", got)
|
||||
}
|
||||
if got := readFileISRC(filepath.Join(dir, "c.wav")); got != "" {
|
||||
t.Fatalf("readFileISRC wav = %q", got)
|
||||
}
|
||||
|
||||
idx := buildISRCIndex(dir)
|
||||
if path, ok := idx.lookup("USAA00000011"); !ok || path != flacPath {
|
||||
t.Fatalf("flac lookup = %q/%v", path, ok)
|
||||
}
|
||||
if path, ok := idx.lookup("USBB00000022"); !ok || path != mp3Path {
|
||||
t.Fatalf("mp3 lookup = %q/%v", path, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestISRCIndexIncrementalRebuild(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
trackA := filepath.Join(dir, "a.flac")
|
||||
|
||||
+34
-2
@@ -108,7 +108,7 @@ func buildISRCIndex(outputDir string) *ISRCIndex {
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
if ext != ".flac" {
|
||||
if !isrcIndexExts[ext] {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ func buildISRCIndex(outputDir string) *ISRCIndex {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := range tasks {
|
||||
isrcs[i] = strings.ToUpper(readFlacISRC(toParse[i].path))
|
||||
isrcs[i] = strings.ToUpper(readFileISRC(toParse[i].path))
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -170,6 +170,38 @@ func buildISRCIndex(outputDir string) *ISRCIndex {
|
||||
return idx
|
||||
}
|
||||
|
||||
// isrcIndexExts are the formats the download pipeline can produce; each has
|
||||
// a native tag reader that stops at the metadata blocks.
|
||||
var isrcIndexExts = map[string]bool{
|
||||
".flac": true,
|
||||
".mp3": true,
|
||||
".m4a": true,
|
||||
".ogg": true,
|
||||
".opus": true,
|
||||
}
|
||||
|
||||
// readFileISRC reads the ISRC tag using the native reader for the format.
|
||||
// Returns "" for unsupported formats, unreadable files, or missing tags.
|
||||
func readFileISRC(path string) string {
|
||||
switch strings.ToLower(filepath.Ext(path)) {
|
||||
case ".flac":
|
||||
return readFlacISRC(path)
|
||||
case ".mp3":
|
||||
if meta, err := ReadID3Tags(path); err == nil && meta != nil {
|
||||
return strings.TrimSpace(meta.ISRC)
|
||||
}
|
||||
case ".m4a":
|
||||
if meta, err := ReadM4ATags(path); err == nil && meta != nil {
|
||||
return strings.TrimSpace(meta.ISRC)
|
||||
}
|
||||
case ".ogg", ".opus":
|
||||
if meta, err := ReadOggVorbisComments(path); err == nil && meta != nil {
|
||||
return strings.TrimSpace(meta.ISRC)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// readFlacISRC extracts the ISRC Vorbis comment from a FLAC file by walking
|
||||
// the metadata block headers and reading only the VORBIS_COMMENT payload;
|
||||
// picture and padding blocks are seeked past, never loaded. Returns "" when
|
||||
|
||||
Reference in New Issue
Block a user