Remove MusicBrainz retry logic — HTTP 200 means no data, not transient error

MusicBrainz always returns HTTP 200; an empty result set is definitive.
Retrying would never yield a different outcome.

- lookup_by_barcode(): retries parameter removed, random import removed
- Removed 3 retry-related tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-02-18 09:48:07 +01:00
commit 2f80cb2693
2 changed files with 11 additions and 58 deletions

View file

@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import random
import time import time
import httpx import httpx
@ -29,50 +28,37 @@ def _get(path: str, params: dict) -> dict:
return response.json() return response.json()
def lookup_by_barcode(ean: str, retries: int = 3) -> Album: def lookup_by_barcode(ean: str) -> Album:
"""Schlägt ein Album anhand des EAN-Barcodes in MusicBrainz nach. """Schlägt ein Album anhand des EAN-Barcodes in MusicBrainz nach.
Führt zwei API-Requests durch: Führt zwei API-Requests durch:
1. Barcode-Suche MBID des ersten Treffers 1. Barcode-Suche MBID des ersten Treffers
2. Release-Details mit Recordings Trackliste 2. Release-Details mit Recordings Trackliste
Bei fehlendem Treffer wird die Suche bis zu `retries`-mal mit MusicBrainz antwortet immer mit HTTP 200; ein leeres Ergebnis bedeutet,
zufälligem Warteabstand (26 s) wiederholt, da MusicBrainz manchmal dass der Barcode nicht bekannt ist ein Retry würde nichts ändern.
beim ersten Versuch keinen Treffer liefert.
Args: Args:
ean: EAN-13- oder UPC-12-Barcode ean: EAN-13- oder UPC-12-Barcode
retries: Anzahl Wiederholungsversuche bei leerem Ergebnis (Standard: 3)
Returns: Returns:
Album mit vollständiger Trackliste Album mit vollständiger Trackliste
Raises: Raises:
ValueError: Kein Eintrag für diesen Barcode gefunden (nach allen Versuchen) ValueError: Kein Eintrag für diesen Barcode gefunden
httpx.HTTPError: Netzwerk- oder API-Fehler httpx.HTTPError: Netzwerk- oder API-Fehler
""" """
for attempt in range(retries + 1): logger.info("MusicBrainz: Suche nach Barcode %s", ean)
# Schritt 1: Barcode-Suche data = _get("/release/", {"query": f"barcode:{ean}", "fmt": "json"})
logger.info("MusicBrainz: Suche nach Barcode %s (Versuch %d/%d)", releases = data.get("releases", [])
ean, attempt + 1, retries + 1)
data = _get("/release/", {"query": f"barcode:{ean}", "fmt": "json"})
releases = data.get("releases", [])
if releases: if not releases:
break
if attempt < retries:
wait = random.uniform(2.0, 6.0)
logger.info("Kein Treffer — warte %.1f s vor erneutem Versuch...", wait)
time.sleep(wait)
else:
raise ValueError(f"Kein MusicBrainz-Eintrag für Barcode {ean!r} gefunden.") raise ValueError(f"Kein MusicBrainz-Eintrag für Barcode {ean!r} gefunden.")
mbid = releases[0]["id"] mbid = releases[0]["id"]
logger.info("MusicBrainz: Treffer MBID=%s, lade Details...", mbid) logger.info("MusicBrainz: Treffer MBID=%s, lade Details...", mbid)
time.sleep(_RATE_SLEEP) time.sleep(_RATE_SLEEP)
# Schritt 2: Trackliste laden
detail = _get(f"/release/{mbid}", {"inc": "recordings", "fmt": "json"}) detail = _get(f"/release/{mbid}", {"inc": "recordings", "fmt": "json"})
return _parse_release(detail) return _parse_release(detail)

View file

@ -171,42 +171,9 @@ class TestLookupByBarcode:
with ( with (
patch("musiksammlung.musicbrainz.httpx.get", return_value=empty), patch("musiksammlung.musicbrainz.httpx.get", return_value=empty),
patch("musiksammlung.musicbrainz.time.sleep"), patch("musiksammlung.musicbrainz.time.sleep"),
patch("musiksammlung.musicbrainz.random.uniform", return_value=0.0),
pytest.raises(ValueError, match="Kein MusicBrainz-Eintrag"), pytest.raises(ValueError, match="Kein MusicBrainz-Eintrag"),
): ):
lookup_by_barcode("0000000000000", retries=0) lookup_by_barcode("0000000000000")
def test_retries_on_empty_result(self) -> None:
"""Bei leerem Ergebnis wird bis zu retries-mal wiederholt."""
empty = _mock_response({"releases": []})
hit = _mock_response(_BARCODE_RESPONSE)
# Erste zwei Versuche leer, dritter Versuch Treffer
responses = [empty, empty, hit, _mock_response(_RELEASE_RESPONSE)]
with (
patch("musiksammlung.musicbrainz.httpx.get", side_effect=responses) as mock_get,
patch("musiksammlung.musicbrainz.time.sleep"),
patch("musiksammlung.musicbrainz.random.uniform", return_value=0.0),
):
album = lookup_by_barcode("0602557360561", retries=3)
assert album.artist == "The Beatles"
# 3 Barcode-Requests + 1 Release-Request
assert mock_get.call_count == 4
def test_retry_sleep_is_called_between_attempts(self) -> None:
"""Zwischen den Versuchen wird time.sleep aufgerufen."""
empty = _mock_response({"releases": []})
responses = [empty, _mock_response(_BARCODE_RESPONSE), _mock_response(_RELEASE_RESPONSE)]
with (
patch("musiksammlung.musicbrainz.httpx.get", side_effect=responses),
patch("musiksammlung.musicbrainz.time.sleep") as mock_sleep,
patch("musiksammlung.musicbrainz.random.uniform", return_value=2.5),
):
lookup_by_barcode("0602557360561", retries=2)
# Einmal Retry-Sleep (2.5 s) + einmal Rate-Limit-Sleep (≥1.1 s)
assert mock_sleep.call_count == 2
assert mock_sleep.call_args_list[0][0][0] == 2.5
def test_uses_first_release(self) -> None: def test_uses_first_release(self) -> None:
barcode_data = {"releases": [{"id": "first-id"}, {"id": "second-id"}]} barcode_data = {"releases": [{"id": "first-id"}, {"id": "second-id"}]}