Jambase aufgeräumt und Backup script
This commit is contained in:
@@ -83,6 +83,28 @@ curl -X PATCH http://127.0.0.1:8001/events/1/purchase \
|
|||||||
- Setze `EVENTLENS_AUTH_USERNAME` und `EVENTLENS_AUTH_PASSWORD`, wenn Eventlens ueber NGINX, VPN oder Tunnel erreichbar ist.
|
- Setze `EVENTLENS_AUTH_USERNAME` und `EVENTLENS_AUTH_PASSWORD`, wenn Eventlens ueber NGINX, VPN oder Tunnel erreichbar ist.
|
||||||
- Das Frontend wird direkt vom FastAPI-Container ausgeliefert, es ist kein Node- oder Build-Container noetig.
|
- Das Frontend wird direkt vom FastAPI-Container ausgeliefert, es ist kein Node- oder Build-Container noetig.
|
||||||
|
|
||||||
|
## Backup und Restore
|
||||||
|
|
||||||
|
Ein komprimiertes Datenbank-Backup kannst du so erstellen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/eventlens
|
||||||
|
bash scripts/backup-db.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Datei landet standardmaessig unter `backups/`. Einen anderen Zielordner kannst du ueber `BACKUP_DIR` setzen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BACKUP_DIR=/srv/backups/eventlens bash scripts/backup-db.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Restore-Beispiel:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gunzip -c backups/eventlens-eventlens-YYYYMMDD-HHMMSS.sql.gz | \
|
||||||
|
docker compose exec -T db mariadb -u"$DB_USER" -p"$DB_PASSWORD" "$DB_NAME"
|
||||||
|
```
|
||||||
|
|
||||||
## Bekannte Betriebsfalle
|
## Bekannte Betriebsfalle
|
||||||
|
|
||||||
Wenn MariaDB bereits mit aelteren Zugangsdaten initialisiert wurde, reicht eine Aenderung in `.env` allein nicht aus. In dem Fall bleibt das Docker-Volume bestehen und der App-User in MariaDB hat noch das alte Passwort.
|
Wenn MariaDB bereits mit aelteren Zugangsdaten initialisiert wurde, reicht eine Aenderung in `.env` allein nicht aus. In dem Fall bleibt das Docker-Volume bestehen und der App-User in MariaDB hat noch das alte Passwort.
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ class JamBaseProvider:
|
|||||||
|
|
||||||
payload = None
|
payload = None
|
||||||
raw_count = 0
|
raw_count = 0
|
||||||
|
self.last_raw_count = 0
|
||||||
|
self.last_match_count = 0
|
||||||
tried_params: list[str] = []
|
tried_params: list[str] = []
|
||||||
bad_request_messages: list[str] = []
|
bad_request_messages: list[str] = []
|
||||||
for params in self._build_param_candidates(term, watch_type, region_scope):
|
for params in self._build_param_candidates(term, watch_type, region_scope):
|
||||||
@@ -74,6 +76,7 @@ class JamBaseProvider:
|
|||||||
return []
|
return []
|
||||||
raw_items = self._extract_items(payload)
|
raw_items = self._extract_items(payload)
|
||||||
raw_count = len(raw_items)
|
raw_count = len(raw_items)
|
||||||
|
self.last_raw_count = raw_count
|
||||||
if raw_items:
|
if raw_items:
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -105,7 +108,13 @@ class JamBaseProvider:
|
|||||||
|
|
||||||
if region_scope == RegionScope.hamburg and normalize_search_text(city) != "hamburg":
|
if region_scope == RegionScope.hamburg and normalize_search_text(city) != "hamburg":
|
||||||
continue
|
continue
|
||||||
if region_scope == RegionScope.germany and country_code not in {"DE", "GER", "Germany"}:
|
if region_scope == RegionScope.germany and country_code not in {
|
||||||
|
"DE",
|
||||||
|
"DEU",
|
||||||
|
"GER",
|
||||||
|
"Germany",
|
||||||
|
"Deutschland",
|
||||||
|
}:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
event_date = self._extract_event_date(item)
|
event_date = self._extract_event_date(item)
|
||||||
@@ -137,6 +146,7 @@ class JamBaseProvider:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.last_match_count = len(results)
|
||||||
self.last_message = (
|
self.last_message = (
|
||||||
f"JamBase lieferte {raw_count} Roh-Events fuer '{term}', "
|
f"JamBase lieferte {raw_count} Roh-Events fuer '{term}', "
|
||||||
f"davon {len(results)} passend nach Eventlens-Filtern."
|
f"davon {len(results)} passend nach Eventlens-Filtern."
|
||||||
@@ -228,7 +238,8 @@ class JamBaseProvider:
|
|||||||
return names
|
return names
|
||||||
|
|
||||||
def _extract_venue_name(self, item: dict) -> str | None:
|
def _extract_venue_name(self, item: dict) -> str | None:
|
||||||
venue = item.get("venue")
|
for key in ("location", "venue"):
|
||||||
|
venue = item.get(key)
|
||||||
if isinstance(venue, dict):
|
if isinstance(venue, dict):
|
||||||
return self._get_first(venue, "name", "venueName", "title")
|
return self._get_first(venue, "name", "venueName", "title")
|
||||||
return self._get_first(item, "venue_name", "venueName")
|
return self._get_first(item, "venue_name", "venueName")
|
||||||
@@ -273,11 +284,6 @@ class JamBaseProvider:
|
|||||||
container = item.get(container_name)
|
container = item.get(container_name)
|
||||||
if not isinstance(container, dict):
|
if not isinstance(container, dict):
|
||||||
continue
|
continue
|
||||||
country = container.get("country")
|
|
||||||
if isinstance(country, dict):
|
|
||||||
return self._get_first(country, "countryCode", "code", "name")
|
|
||||||
if isinstance(country, str):
|
|
||||||
return country
|
|
||||||
address = container.get("address")
|
address = container.get("address")
|
||||||
if isinstance(address, dict):
|
if isinstance(address, dict):
|
||||||
country = address.get("addressCountry")
|
country = address.get("addressCountry")
|
||||||
@@ -286,6 +292,11 @@ class JamBaseProvider:
|
|||||||
if isinstance(country, str):
|
if isinstance(country, str):
|
||||||
return country
|
return country
|
||||||
return self._get_first(address, "countryCode", "country")
|
return self._get_first(address, "countryCode", "country")
|
||||||
|
country = container.get("country")
|
||||||
|
if isinstance(country, dict):
|
||||||
|
return self._get_first(country, "countryCode", "code", "identifier", "name")
|
||||||
|
if isinstance(country, str):
|
||||||
|
return country
|
||||||
return self._get_first(item, "country_code", "countryCode", "country")
|
return self._get_first(item, "country_code", "countryCode", "country")
|
||||||
|
|
||||||
def _extract_event_date(self, item: dict) -> datetime | None:
|
def _extract_event_date(self, item: dict) -> datetime | None:
|
||||||
@@ -326,7 +337,7 @@ class JamBaseProvider:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _extract_image_url(self, item: dict) -> str | None:
|
def _extract_image_url(self, item: dict) -> str | None:
|
||||||
for key in ("image", "image_url", "imageUrl"):
|
for key in ("x-promoImage", "image", "image_url", "imageUrl"):
|
||||||
value = item.get(key)
|
value = item.get(key)
|
||||||
if isinstance(value, str) and value:
|
if isinstance(value, str) and value:
|
||||||
return value
|
return value
|
||||||
|
|||||||
+25
-4
@@ -25,7 +25,8 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
PROVIDER_PRIORITY = {
|
PROVIDER_PRIORITY = {
|
||||||
"ticketmaster": 3,
|
"ticketmaster": 4,
|
||||||
|
"jambase": 3,
|
||||||
"eventim": 2,
|
"eventim": 2,
|
||||||
"bandsintown": 1,
|
"bandsintown": 1,
|
||||||
}
|
}
|
||||||
@@ -126,6 +127,8 @@ def init_provider_sync_state(provider_name: str) -> dict:
|
|||||||
"ok_count": 0,
|
"ok_count": 0,
|
||||||
"blocked_terms": [],
|
"blocked_terms": [],
|
||||||
"error_terms": [],
|
"error_terms": [],
|
||||||
|
"event_count": 0,
|
||||||
|
"raw_event_count": 0,
|
||||||
"last_message": "",
|
"last_message": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,12 +138,17 @@ def record_provider_sync_state(
|
|||||||
status: ProviderStatusType,
|
status: ProviderStatusType,
|
||||||
term: str,
|
term: str,
|
||||||
message: str,
|
message: str,
|
||||||
|
event_count: int = 0,
|
||||||
|
raw_event_count: int | None = None,
|
||||||
):
|
):
|
||||||
if PROVIDER_STATUS_PRIORITY[status] > PROVIDER_STATUS_PRIORITY[state["status"]]:
|
if PROVIDER_STATUS_PRIORITY[status] > PROVIDER_STATUS_PRIORITY[state["status"]]:
|
||||||
state["status"] = status
|
state["status"] = status
|
||||||
|
|
||||||
if status == ProviderStatusType.ok:
|
if status == ProviderStatusType.ok:
|
||||||
state["ok_count"] += 1
|
state["ok_count"] += 1
|
||||||
|
state["event_count"] += event_count
|
||||||
|
if raw_event_count is not None:
|
||||||
|
state["raw_event_count"] += raw_event_count
|
||||||
elif status == ProviderStatusType.blocked:
|
elif status == ProviderStatusType.blocked:
|
||||||
if term not in state["blocked_terms"]:
|
if term not in state["blocked_terms"]:
|
||||||
state["blocked_terms"].append(term)
|
state["blocked_terms"].append(term)
|
||||||
@@ -166,7 +174,12 @@ def build_provider_status_message(state: dict) -> str:
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
f"{state['provider_name']} erfolgreich fuer {state['ok_count']} "
|
f"{state['provider_name']} erfolgreich fuer {state['ok_count']} "
|
||||||
f"Watchlist-Eintraege geprueft."
|
f"Watchlist-Eintraege geprueft, {state['event_count']} passende Events."
|
||||||
|
+ (
|
||||||
|
f" {state['raw_event_count']} Roh-Events ausgewertet."
|
||||||
|
if state["raw_event_count"]
|
||||||
|
else ""
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -214,19 +227,25 @@ def events_are_equivalent(left: TrackedEvent, right: TrackedEvent) -> bool:
|
|||||||
def is_preferred_event(candidate: TrackedEvent, current: TrackedEvent) -> bool:
|
def is_preferred_event(candidate: TrackedEvent, current: TrackedEvent) -> bool:
|
||||||
candidate_score = (
|
candidate_score = (
|
||||||
1 if candidate.is_ticket_purchased else 0,
|
1 if candidate.is_ticket_purchased else 0,
|
||||||
PROVIDER_PRIORITY.get(candidate.source, 0),
|
get_provider_priority(candidate.source),
|
||||||
1 if candidate.ticket_url else 0,
|
1 if candidate.ticket_url else 0,
|
||||||
candidate.last_seen_at or datetime.min,
|
candidate.last_seen_at or datetime.min,
|
||||||
)
|
)
|
||||||
current_score = (
|
current_score = (
|
||||||
1 if current.is_ticket_purchased else 0,
|
1 if current.is_ticket_purchased else 0,
|
||||||
PROVIDER_PRIORITY.get(current.source, 0),
|
get_provider_priority(current.source),
|
||||||
1 if current.ticket_url else 0,
|
1 if current.ticket_url else 0,
|
||||||
current.last_seen_at or datetime.min,
|
current.last_seen_at or datetime.min,
|
||||||
)
|
)
|
||||||
return candidate_score > current_score
|
return candidate_score > current_score
|
||||||
|
|
||||||
|
|
||||||
|
def get_provider_priority(source: str | None) -> int:
|
||||||
|
if source and source.startswith("source:"):
|
||||||
|
return 5
|
||||||
|
return PROVIDER_PRIORITY.get(source or "", 0)
|
||||||
|
|
||||||
|
|
||||||
def has_equivalent_existing_event(db: Session, tracked_event: TrackedEvent) -> bool:
|
def has_equivalent_existing_event(db: Session, tracked_event: TrackedEvent) -> bool:
|
||||||
stmt = select(TrackedEvent).where(
|
stmt = select(TrackedEvent).where(
|
||||||
TrackedEvent.watch_item_id == tracked_event.watch_item_id,
|
TrackedEvent.watch_item_id == tracked_event.watch_item_id,
|
||||||
@@ -411,6 +430,8 @@ def run_sync(db: Session) -> SyncResult:
|
|||||||
status=ProviderStatusType(provider_status_name),
|
status=ProviderStatusType(provider_status_name),
|
||||||
term=watch_item.name,
|
term=watch_item.name,
|
||||||
message=provider_message,
|
message=provider_message,
|
||||||
|
event_count=len(events),
|
||||||
|
raw_event_count=getattr(provider, "last_raw_count", None),
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
provider_message = getattr(
|
provider_message = getattr(
|
||||||
|
|||||||
Executable
+46
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
BACKUP_DIR="${BACKUP_DIR:-${PROJECT_DIR}/backups}"
|
||||||
|
|
||||||
|
if [[ ! -f "${PROJECT_DIR}/.env" ]]; then
|
||||||
|
echo ".env wurde in ${PROJECT_DIR} nicht gefunden." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
read_env_value() {
|
||||||
|
local key="$1"
|
||||||
|
local line
|
||||||
|
|
||||||
|
line="$(grep -E "^${key}=" "${PROJECT_DIR}/.env" | tail -n 1 || true)"
|
||||||
|
if [[ -z "${line}" ]]; then
|
||||||
|
echo "Wert ${key} wurde in ${PROJECT_DIR}/.env nicht gefunden." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "${line#*=}"
|
||||||
|
}
|
||||||
|
|
||||||
|
DB_NAME="$(read_env_value DB_NAME)"
|
||||||
|
DB_USER="$(read_env_value DB_USER)"
|
||||||
|
DB_PASSWORD="$(read_env_value DB_PASSWORD)"
|
||||||
|
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
BACKUP_FILE="${BACKUP_DIR}/eventlens-${DB_NAME}-${TIMESTAMP}.sql.gz"
|
||||||
|
|
||||||
|
mkdir -p "${BACKUP_DIR}"
|
||||||
|
|
||||||
|
docker compose -f "${PROJECT_DIR}/docker-compose.yml" exec -T db \
|
||||||
|
mariadb-dump \
|
||||||
|
--single-transaction \
|
||||||
|
--quick \
|
||||||
|
--routines \
|
||||||
|
--triggers \
|
||||||
|
-u"${DB_USER}" \
|
||||||
|
"-p${DB_PASSWORD}" \
|
||||||
|
"${DB_NAME}" \
|
||||||
|
| gzip -c > "${BACKUP_FILE}"
|
||||||
|
|
||||||
|
echo "Backup erstellt: ${BACKUP_FILE}"
|
||||||
Reference in New Issue
Block a user