from datetime import datetime from urllib.parse import quote import requests from app.config import settings from app.models import RegionScope, WatchType from app.providers.utils import normalize_search_text class BandsintownProvider: base_url = "https://rest.bandsintown.com" source_name = "bandsintown" def is_configured(self) -> bool: return bool(settings.bandsintown_app_id) def search_events( self, term: str, watch_type: WatchType, region_scope: RegionScope, ) -> list[dict]: if not self.is_configured(): setattr(self, "last_status", "ok") setattr( self, "last_message", "Bandsintown ist nicht konfiguriert. Bitte eine gueltige BANDSINTOWN_APP_ID setzen.", ) return [] if watch_type != WatchType.artist: setattr(self, "last_status", "ok") setattr( self, "last_message", f"Bandsintown skipped non-artist watch item '{term}'.", ) return [] artist_name = quote(term, safe="") url = f"{self.base_url}/artists/{artist_name}/events" response = requests.get( url, params={"app_id": settings.bandsintown_app_id, "date": "upcoming"}, timeout=30, ) if response.status_code in {401, 403, 429}: setattr(self, "last_status", "blocked") setattr( self, "last_message", ( f"Bandsintown rejected app_id for '{term}' " f"with status {response.status_code}." ), ) return [] if response.status_code == 404: setattr(self, "last_status", "ok") setattr( self, "last_message", f"Bandsintown found no artist match for '{term}'.", ) return [] response.raise_for_status() payload = response.json() if not isinstance(payload, list): setattr(self, "last_status", "error") setattr( self, "last_message", f"Bandsintown returned an unexpected payload for '{term}'.", ) return [] setattr(self, "last_status", "ok") setattr( self, "last_message", f"Bandsintown returned {len(payload)} raw events for term '{term}'.", ) results: list[dict] = [] normalized_term = normalize_search_text(term) for item in payload: venue = item.get("venue") or {} city = venue.get("city") country = venue.get("country") if region_scope == RegionScope.hamburg and (city or "").casefold() != "hamburg": continue if region_scope == RegionScope.germany and (country or "").casefold() != "germany": continue datetime_value = item.get("datetime") event_date = None if datetime_value: try: event_date = datetime.fromisoformat( datetime_value.replace("Z", "+00:00") ).replace(tzinfo=None) except ValueError: event_date = None offers = item.get("offers") or [] ticket_url = next((offer.get("url") for offer in offers if offer.get("url")), None) title = item.get("title") or f"{term} live" artist_name = item.get("artist", {}).get("name", term) haystack = normalize_search_text(f"{artist_name} {title}") if normalized_term not in haystack: continue results.append( { "external_id": str(item.get("id")), "title": title, "matched_term": term, "venue_name": venue.get("name"), "city": city, "country_code": "DE" if country == "Germany" else country, "event_date": event_date, "ticket_url": ticket_url or item.get("url"), "image_url": item.get("artist", {}).get("thumb_url"), "raw_payload": item, } ) return results