diff --git a/src/musiksammlung/musicbrainz.py b/src/musiksammlung/musicbrainz.py index c3c9242..0902b92 100644 --- a/src/musiksammlung/musicbrainz.py +++ b/src/musiksammlung/musicbrainz.py @@ -3,7 +3,6 @@ from __future__ import annotations import logging -import random import time import httpx @@ -29,50 +28,37 @@ def _get(path: str, params: dict) -> dict: 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. Führt zwei API-Requests durch: 1. Barcode-Suche → MBID des ersten Treffers 2. Release-Details mit Recordings → Trackliste - Bei fehlendem Treffer wird die Suche bis zu `retries`-mal mit - zufälligem Warteabstand (2–6 s) wiederholt, da MusicBrainz manchmal - beim ersten Versuch keinen Treffer liefert. + MusicBrainz antwortet immer mit HTTP 200; ein leeres Ergebnis bedeutet, + dass der Barcode nicht bekannt ist — ein Retry würde nichts ändern. Args: - ean: EAN-13- oder UPC-12-Barcode - retries: Anzahl Wiederholungsversuche bei leerem Ergebnis (Standard: 3) + ean: EAN-13- oder UPC-12-Barcode Returns: Album mit vollständiger Trackliste 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 """ - for attempt in range(retries + 1): - # Schritt 1: Barcode-Suche - logger.info("MusicBrainz: Suche nach Barcode %s (Versuch %d/%d)", - ean, attempt + 1, retries + 1) - data = _get("/release/", {"query": f"barcode:{ean}", "fmt": "json"}) - releases = data.get("releases", []) + logger.info("MusicBrainz: Suche nach Barcode %s", ean) + data = _get("/release/", {"query": f"barcode:{ean}", "fmt": "json"}) + releases = data.get("releases", []) - if 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: + if not releases: raise ValueError(f"Kein MusicBrainz-Eintrag für Barcode {ean!r} gefunden.") mbid = releases[0]["id"] logger.info("MusicBrainz: Treffer MBID=%s, lade Details...", mbid) time.sleep(_RATE_SLEEP) - # Schritt 2: Trackliste laden detail = _get(f"/release/{mbid}", {"inc": "recordings", "fmt": "json"}) return _parse_release(detail) diff --git a/tests/test_musicbrainz.py b/tests/test_musicbrainz.py index c16ae21..41308dd 100644 --- a/tests/test_musicbrainz.py +++ b/tests/test_musicbrainz.py @@ -171,42 +171,9 @@ class TestLookupByBarcode: with ( patch("musiksammlung.musicbrainz.httpx.get", return_value=empty), patch("musiksammlung.musicbrainz.time.sleep"), - patch("musiksammlung.musicbrainz.random.uniform", return_value=0.0), pytest.raises(ValueError, match="Kein MusicBrainz-Eintrag"), ): - lookup_by_barcode("0000000000000", retries=0) - - 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 + lookup_by_barcode("0000000000000") def test_uses_first_release(self) -> None: barcode_data = {"releases": [{"id": "first-id"}, {"id": "second-id"}]}