51 lines
1.5 KiB
Python
51 lines
1.5 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
MAX_SECONDS = 120
|
|
MAX_BIT_ERROR = 2
|
|
MAX_ALIGN_OFFSET = 120
|
|
|
|
|
|
def chromaprint(path: Path) -> str | None:
|
|
try:
|
|
p = subprocess.run(
|
|
["fpcalc", "-length", str(MAX_SECONDS), "-json", str(path)],
|
|
capture_output=True, text=True, timeout=180, check=True,
|
|
)
|
|
value = json.loads(p.stdout).get("fingerprint")
|
|
return value if isinstance(value, str) and value else None
|
|
except (OSError, subprocess.SubprocessError, ValueError, json.JSONDecodeError):
|
|
return None
|
|
|
|
|
|
def _popcount(value: int) -> int:
|
|
return value.bit_count()
|
|
|
|
|
|
def similarity(a: str, b: str) -> float:
|
|
"""Compare Chromaprint fingerprints using the AcoustID alignment method.
|
|
|
|
This is deliberately bounded to the local fingerprint comparison and does
|
|
not contact the AcoustID service.
|
|
"""
|
|
if not a or not b:
|
|
return 0.0
|
|
try:
|
|
import chromaprint as cp
|
|
aa = [int(x) for x in cp.decode_fingerprint(a.encode())[0]]
|
|
bb = [int(x) for x in cp.decode_fingerprint(b.encode())[0]]
|
|
except (ImportError, AttributeError, TypeError, ValueError):
|
|
return 0.0
|
|
if not aa or not bb:
|
|
return 0.0
|
|
|
|
counts = [0] * (len(aa) + len(bb) + 1)
|
|
for i, av in enumerate(aa):
|
|
for j in range(max(0, i - MAX_ALIGN_OFFSET), min(len(bb), i + MAX_ALIGN_OFFSET)):
|
|
if _popcount(av ^ bb[j]) <= MAX_BIT_ERROR:
|
|
counts[i - j + len(bb)] += 1
|
|
return max(counts) / min(len(aa), len(bb))
|