fix: a track shorter than the fingerprint span keeps its print (#6178)
release / govulncheck (push) Successful in 16s
release / web (push) Successful in 1m26s
release / go (push) Successful in 1m43s
release / integration (push) Successful in 5m14s
release / android (push) Successful in 5m15s
release / Build signed APK (releases and dev) (push) Successful in 5m20s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m18s
release / Verify release artifacts (tag releases only) (push) Skipped
release / govulncheck (push) Successful in 16s
release / web (push) Successful in 1m26s
release / go (push) Successful in 1m43s
release / integration (push) Successful in 5m14s
release / android (push) Successful in 5m15s
release / Build signed APK (releases and dev) (push) Successful in 5m20s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m18s
release / Verify release artifacts (tag releases only) (push) Skipped
The image's fpcalc 1.5.1 exits 3 with "Error decoding audio frame (End of file)" whenever it reaches the end of a file before -length, at any length, so every track under two minutes failed both the AcoustID lookup print and the duplicate-detection print: 471 lookups and all 1,979 present short tracks' chromaprints. - runFingerprintTool returns the tool's stdout beside a toolExitError. - fpcalcReadToEnd keeps a partial exit whose stderr is the end of file and whose declared DURATION is within the span asked for. A damaged file that stops early declares more than it read, and still fails. - Migration 0097 clears the stored failures so the lookup worker and the fingerprint backfill take those tracks again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -264,7 +264,7 @@ func computeAudioStreamSHA256(ctx context.Context, path string) ([]byte, error)
|
||||
// lengthSec seconds of the file.
|
||||
func computeChromaprint(ctx context.Context, path string, lengthSec int32) ([]int32, error) {
|
||||
out, err := runFingerprintTool(ctx, "fpcalc", fpcalcArgs(path, int(lengthSec)))
|
||||
if err != nil {
|
||||
if err != nil && !fpcalcReadToEnd(err, out, int(lengthSec)) {
|
||||
return nil, err
|
||||
}
|
||||
return parseFpcalcRaw(out)
|
||||
@@ -276,7 +276,7 @@ func computeChromaprint(ctx context.Context, path string, lengthSec int32) ([]in
|
||||
// rejects as a verdict.
|
||||
func computeLookupFingerprint(ctx context.Context, path string) (lookupFingerprint, error) {
|
||||
out, err := runFingerprintTool(ctx, "fpcalc", fpcalcLookupArgs(path))
|
||||
if err != nil {
|
||||
if err != nil && !fpcalcReadToEnd(err, out, lookupFingerprintLengthSec) {
|
||||
return lookupFingerprint{}, err
|
||||
}
|
||||
return parseFpcalcCompressed(out)
|
||||
@@ -289,6 +289,9 @@ func computeLookupFingerprint(ctx context.Context, path string) (lookupFingerpri
|
||||
// fingerprint of a damaged file is not that file's identity. Stored, it would
|
||||
// score against a healthy copy over whatever prefix survived, and could group
|
||||
// or fail to group either way. Absent is better than wrong.
|
||||
//
|
||||
// A tool that ran and exited non-zero returns its stdout beside the
|
||||
// *toolExitError, for the one exit 3 that is not damage (fpcalcReadToEnd).
|
||||
func runFingerprintTool(ctx context.Context, name string, args []string) ([]byte, error) {
|
||||
runCtx, cancel := context.WithTimeout(ctx, fingerprintTimeout)
|
||||
defer cancel()
|
||||
@@ -311,11 +314,58 @@ func runFingerprintTool(ctx context.Context, name string, args []string) ([]byte
|
||||
}
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
return nil, fmt.Errorf("%s exited %d: %s", name, exitErr.ExitCode(), stderrTail(exitErr.Stderr))
|
||||
return out, &toolExitError{name: name, code: exitErr.ExitCode(), stderr: stderrTail(exitErr.Stderr)}
|
||||
}
|
||||
return nil, fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
|
||||
// toolExitError is a fingerprint tool that ran and exited non-zero: a verdict
|
||||
// on the file, unlike a stall or a missing tool.
|
||||
type toolExitError struct {
|
||||
name string
|
||||
code int
|
||||
stderr []byte // the tail, as stderrTail keeps it
|
||||
}
|
||||
|
||||
func (e *toolExitError) Error() string {
|
||||
return fmt.Sprintf("%s exited %d: %s", e.name, e.code, e.stderr)
|
||||
}
|
||||
|
||||
// fpcalcExitPartial is fpcalc's exit for "reading failed, but here is a
|
||||
// fingerprint of what I got".
|
||||
const fpcalcExitPartial = 3
|
||||
|
||||
// fpcalcReadToEnd reports whether fpcalc's partial exit only means it decoded
|
||||
// the whole file (#6178). The image's fpcalc 1.5.1 reports the decoder's end of
|
||||
// file as "Error decoding audio frame (End of file)" and exits 3 whenever it
|
||||
// reaches the end before -length, so it failed every file shorter than the
|
||||
// span asked for: every track under two minutes, at any length setting. That
|
||||
// print covers the whole file, so it is the file's identity.
|
||||
//
|
||||
// A damaged file can stop at an end of file too. Its DURATION, the length the
|
||||
// container declares, is then longer than the span asked for, and it stays a
|
||||
// failure.
|
||||
func fpcalcReadToEnd(err error, out []byte, lengthSec int) bool {
|
||||
var exitErr *toolExitError
|
||||
if !errors.As(err, &exitErr) || exitErr.code != fpcalcExitPartial ||
|
||||
!bytes.Contains(exitErr.stderr, []byte("(End of file)")) {
|
||||
return false
|
||||
}
|
||||
d, ok := fpcalcDuration(out)
|
||||
return ok && d > 0 && d <= lengthSec
|
||||
}
|
||||
|
||||
// fpcalcDuration reads the DURATION= line of fpcalc's output, in whole seconds.
|
||||
func fpcalcDuration(out []byte) (int, bool) {
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
if v, ok := strings.CutPrefix(strings.TrimSpace(line), "DURATION="); ok {
|
||||
d, err := strconv.Atoi(v)
|
||||
return d, err == nil
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// stderrTail keeps the END of a failing tool's stderr. ffmpeg and fpcalc print
|
||||
// the actual reason last, after any banner or per-frame warnings, so a cap that
|
||||
// kept the head would log the noise and drop the cause.
|
||||
|
||||
Reference in New Issue
Block a user