from datetime import datetime import logging import requests from app.config import settings from app.models import RegionScope, WatchType from app.providers.utils import normalize_search_text logger = logging.getLogger(__name__) class EventimProvider: base_url = "https://public-api.eventim.com/websearch/search/api/exploration/v1/products" source_name = "eventim" def is_configured(self) -> bool: return settings.eventim_enabled def search_events( self, term: str, watch_type: WatchType, region_scope: RegionScope, ) -> list[dict]: if not self.is_configured(): return [] params = { "webId": "web__eventim-de", "language": "de", "page": 1, "sort": "DateAsc", "top": 50, "search_term": term, } if region_scope == RegionScope.hamburg: params["city_names"] = "Hamburg" response = requests.get( self.base_url, params=params, headers={ "Accept": "application/json", "User-Agent": ( "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" ), }, timeout=30, ) if response.status_code in {403, 429}: logger.warning( "Eventim API blocked request with status %s for term '%s'.", response.status_code, term, ) setattr(self, "last_status", "blocked") setattr( self, "last_message", f"Eventim API blocked request with status {response.status_code} for term '{term}'.", ) return [] response.raise_for_status() payload = response.json() products = payload.get("products") or [] setattr(self, "last_status", "ok") setattr( self, "last_message", f"Eventim returned {len(products)} raw products for term '{term}'.", ) results: list[dict] = [] normalized_term = normalize_search_text(term) for product in products: title = product.get("name") or "" attractions = product.get("attractions") or [] attraction_names = [entry.get("name", "") for entry in attractions] if watch_type == WatchType.artist: haystack = normalize_search_text(" ".join(attraction_names + [title])) if normalized_term not in haystack: continue elif normalized_term not in normalize_search_text(title): continue live_data = product.get("typeAttributes", {}).get("liveEntertainment", {}) location = live_data.get("location") or {} city = location.get("city") if region_scope == RegionScope.hamburg and (city or "").casefold() != "hamburg": continue event_date = None start_date = live_data.get("startDate") if start_date: try: event_date = datetime.fromisoformat( start_date.replace("Z", "+00:00") ).replace(tzinfo=None) except ValueError: event_date = None url = self._build_url(product) country_code = "DE" if region_scope == RegionScope.germany or city else None results.append( { "external_id": str(product.get("productId") or url or title), "title": title, "matched_term": term, "venue_name": location.get("name"), "city": city, "country_code": country_code, "event_date": event_date, "ticket_url": url, "image_url": None, "raw_payload": product, } ) return results def _build_url(self, product: dict) -> str | None: direct_link = product.get("link") if direct_link: return direct_link url = product.get("url") or {} domain = url.get("domain") path = url.get("path") if domain and path: return f"{domain}{path}" return None