Fix MusicBrainz release matching preferring standard over deluxe

When searching for "Playing the Angel (Deluxe)", MusicBrainz would
match the standard release because its higher popularity score
outweighed the lower title similarity. Now match_release extracts
version qualifiers (Deluxe, Remastered, etc.) from both the query
and each result, applying a +10 bonus for exact qualifier match,
+5 for partial, and -5 penalty when the query has a qualifier but
the result doesn't. This ensures deluxe downloads get the deluxe
MBID, preventing Navidrome from splitting albums.
This commit is contained in:
Broque Thomas 2026-03-21 10:48:35 -07:00
parent 85044261a4
commit f991d02f0c

View file

@ -1,5 +1,6 @@
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
import json import json
import re
from datetime import datetime, timedelta from datetime import datetime, timedelta
from difflib import SequenceMatcher from difflib import SequenceMatcher
from utils.logging_config import get_logger from utils.logging_config import get_logger
@ -186,6 +187,18 @@ class MusicBrainzService:
logger.error(f"Error matching artist '{artist_name}': {e}") logger.error(f"Error matching artist '{artist_name}': {e}")
return None return None
# Version qualifiers that distinguish releases (Deluxe, Remastered, etc.)
_VERSION_QUALIFIERS = re.compile(
r'\b(deluxe|expanded|remaster(?:ed)?|anniversary|special|collector|'
r'limited|bonus|platinum|gold|super\s*deluxe|standard)\b',
re.IGNORECASE
)
def _extract_version_qualifier(self, title: str) -> str:
"""Extract version qualifiers from an album title, normalized and sorted."""
qualifiers = sorted(set(q.lower() for q in self._VERSION_QUALIFIERS.findall(title)))
return ' '.join(qualifiers)
def match_release(self, album_name: str, artist_name: Optional[str] = None) -> Optional[Dict[str, Any]]: def match_release(self, album_name: str, artist_name: Optional[str] = None) -> Optional[Dict[str, Any]]:
""" """
Match a release (album) by name to MusicBrainz Match a release (album) by name to MusicBrainz
@ -213,6 +226,9 @@ class MusicBrainzService:
self._save_to_cache('release', album_name, artist_name, None, None, 0) self._save_to_cache('release', album_name, artist_name, None, None, 0)
return None return None
# Extract version qualifier from search query for preference matching
query_qualifier = self._extract_version_qualifier(album_name)
# Find best match # Find best match
best_match = None best_match = None
best_confidence = 0 best_confidence = 0
@ -236,8 +252,22 @@ class MusicBrainzService:
artist_bonus = 20 artist_bonus = 20
break break
# Version qualifier matching: prefer releases with the same
# edition qualifier (Deluxe, Remastered, etc.) as the query.
# This prevents "Playing the Angel (Deluxe)" from matching the
# standard "Playing the Angel" release.
version_bonus = 0
if query_qualifier:
mb_qualifier = self._extract_version_qualifier(mb_title)
if query_qualifier == mb_qualifier:
version_bonus = 10 # Same edition — strong preference
elif mb_qualifier and mb_qualifier in query_qualifier:
version_bonus = 5 # Partial match (e.g. "deluxe" in "super deluxe")
elif not mb_qualifier:
version_bonus = -5 # Query has qualifier but result doesn't — penalize
# Combine scores - cap at 100 # Combine scores - cap at 100
confidence = min(100, int((title_similarity * 50) + (mb_score / 100 * 30) + artist_bonus)) confidence = min(100, int((title_similarity * 50) + (mb_score / 100 * 30) + artist_bonus + version_bonus))
if confidence > best_confidence: if confidence > best_confidence:
best_confidence = confidence best_confidence = confidence