feat: retry MusicBrainz barcode lookup with random delay on empty result
Up to 3 retries with 2–6 s random wait between attempts, as MusicBrainz occasionally returns no results on the first try. retries parameter is configurable (default: 3). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
468fac6d2b
commit
65164d428c
2 changed files with 58 additions and 10 deletions
|
|
@ -3,6 +3,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import random
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
@ -28,29 +29,43 @@ def _get(path: str, params: dict) -> dict:
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
def lookup_by_barcode(ean: str) -> Album:
|
def lookup_by_barcode(ean: str, retries: int = 3) -> 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
|
||||||
|
zufälligem Warteabstand (2–6 s) wiederholt, da MusicBrainz manchmal
|
||||||
|
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
|
ValueError: Kein Eintrag für diesen Barcode gefunden (nach allen Versuchen)
|
||||||
httpx.HTTPError: Netzwerk- oder API-Fehler
|
httpx.HTTPError: Netzwerk- oder API-Fehler
|
||||||
"""
|
"""
|
||||||
# Schritt 1: Barcode-Suche
|
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)",
|
||||||
|
ean, attempt + 1, retries + 1)
|
||||||
|
data = _get("/release/", {"query": f"barcode:{ean}", "fmt": "json"})
|
||||||
|
releases = data.get("releases", [])
|
||||||
|
|
||||||
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"]
|
||||||
|
|
|
||||||
|
|
@ -171,9 +171,42 @@ 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")
|
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
|
||||||
|
|
||||||
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"}]}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue