Persist Seerr media failure suppression and reduce sync error noise

This commit is contained in:
2026-03-01 22:53:38 +13:00
parent aae2c3d418
commit b068a6066e
8 changed files with 389 additions and 67 deletions

View File

@@ -11,6 +11,11 @@ from .security import hash_password, verify_password
logger = logging.getLogger(__name__)
SEERR_MEDIA_FAILURE_SHORT_SUPPRESS_HOURS = 6
SEERR_MEDIA_FAILURE_RETRY_SUPPRESS_HOURS = 24
SEERR_MEDIA_FAILURE_PERSISTENT_SUPPRESS_DAYS = 30
SEERR_MEDIA_FAILURE_PERSISTENT_THRESHOLD = 3
def _db_path() -> str:
path = settings.sqlite_path or "data/magent.db"
@@ -271,6 +276,22 @@ def init_db() -> None:
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS seerr_media_failures (
media_type TEXT NOT NULL,
tmdb_id INTEGER NOT NULL,
status_code INTEGER,
error_message TEXT,
failure_count INTEGER NOT NULL DEFAULT 1,
first_failed_at TEXT NOT NULL,
last_failed_at TEXT NOT NULL,
suppress_until TEXT NOT NULL,
is_persistent INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (media_type, tmdb_id)
)
"""
)
conn.execute(
"""
CREATE INDEX IF NOT EXISTS idx_requests_cache_created_at
@@ -289,6 +310,12 @@ def init_db() -> None:
ON artwork_cache_status (updated_at)
"""
)
conn.execute(
"""
CREATE INDEX IF NOT EXISTS idx_seerr_media_failures_suppress_until
ON seerr_media_failures (suppress_until)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS user_activity (
@@ -2226,6 +2253,154 @@ def get_settings_overrides() -> Dict[str, str]:
return overrides
def get_seerr_media_failure(media_type: Optional[str], tmdb_id: Optional[int]) -> Optional[Dict[str, Any]]:
if not media_type or not tmdb_id:
return None
normalized_media_type = str(media_type).strip().lower()
try:
normalized_tmdb_id = int(tmdb_id)
except (TypeError, ValueError):
return None
with _connect() as conn:
row = conn.execute(
"""
SELECT media_type, tmdb_id, status_code, error_message, failure_count,
first_failed_at, last_failed_at, suppress_until, is_persistent
FROM seerr_media_failures
WHERE media_type = ? AND tmdb_id = ?
""",
(normalized_media_type, normalized_tmdb_id),
).fetchone()
if not row:
return None
return {
"media_type": row[0],
"tmdb_id": row[1],
"status_code": row[2],
"error_message": row[3],
"failure_count": row[4],
"first_failed_at": row[5],
"last_failed_at": row[6],
"suppress_until": row[7],
"is_persistent": bool(row[8]),
}
def is_seerr_media_failure_suppressed(media_type: Optional[str], tmdb_id: Optional[int]) -> bool:
record = get_seerr_media_failure(media_type, tmdb_id)
if not record:
return False
suppress_until = _parse_datetime_value(record.get("suppress_until"))
if suppress_until and suppress_until > datetime.now(timezone.utc):
return True
clear_seerr_media_failure(media_type, tmdb_id)
return False
def record_seerr_media_failure(
media_type: Optional[str],
tmdb_id: Optional[int],
*,
status_code: Optional[int] = None,
error_message: Optional[str] = None,
) -> Dict[str, Any]:
if not media_type or not tmdb_id:
return {}
normalized_media_type = str(media_type).strip().lower()
normalized_tmdb_id = int(tmdb_id)
now = datetime.now(timezone.utc)
existing = get_seerr_media_failure(normalized_media_type, normalized_tmdb_id)
failure_count = int(existing.get("failure_count", 0)) + 1 if existing else 1
is_persistent = failure_count >= SEERR_MEDIA_FAILURE_PERSISTENT_THRESHOLD
if is_persistent:
suppress_until = now + timedelta(days=SEERR_MEDIA_FAILURE_PERSISTENT_SUPPRESS_DAYS)
elif failure_count >= 2:
suppress_until = now + timedelta(hours=SEERR_MEDIA_FAILURE_RETRY_SUPPRESS_HOURS)
else:
suppress_until = now + timedelta(hours=SEERR_MEDIA_FAILURE_SHORT_SUPPRESS_HOURS)
payload = {
"media_type": normalized_media_type,
"tmdb_id": normalized_tmdb_id,
"status_code": status_code,
"error_message": error_message,
"failure_count": failure_count,
"first_failed_at": existing.get("first_failed_at") if existing else now.isoformat(),
"last_failed_at": now.isoformat(),
"suppress_until": suppress_until.isoformat(),
"is_persistent": is_persistent,
}
with _connect() as conn:
conn.execute(
"""
INSERT INTO seerr_media_failures (
media_type,
tmdb_id,
status_code,
error_message,
failure_count,
first_failed_at,
last_failed_at,
suppress_until,
is_persistent
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(media_type, tmdb_id) DO UPDATE SET
status_code = excluded.status_code,
error_message = excluded.error_message,
failure_count = excluded.failure_count,
first_failed_at = excluded.first_failed_at,
last_failed_at = excluded.last_failed_at,
suppress_until = excluded.suppress_until,
is_persistent = excluded.is_persistent
""",
(
payload["media_type"],
payload["tmdb_id"],
payload["status_code"],
payload["error_message"],
payload["failure_count"],
payload["first_failed_at"],
payload["last_failed_at"],
payload["suppress_until"],
1 if payload["is_persistent"] else 0,
),
)
logger.warning(
"seerr_media_failure upsert: media_type=%s tmdb_id=%s status=%s failure_count=%s suppress_until=%s persistent=%s",
payload["media_type"],
payload["tmdb_id"],
payload["status_code"],
payload["failure_count"],
payload["suppress_until"],
payload["is_persistent"],
)
return payload
def clear_seerr_media_failure(media_type: Optional[str], tmdb_id: Optional[int]) -> None:
if not media_type or not tmdb_id:
return
normalized_media_type = str(media_type).strip().lower()
try:
normalized_tmdb_id = int(tmdb_id)
except (TypeError, ValueError):
return
with _connect() as conn:
deleted = conn.execute(
"""
DELETE FROM seerr_media_failures
WHERE media_type = ? AND tmdb_id = ?
""",
(normalized_media_type, normalized_tmdb_id),
).rowcount
if deleted:
logger.info(
"seerr_media_failure cleared: media_type=%s tmdb_id=%s",
normalized_media_type,
normalized_tmdb_id,
)
def run_integrity_check() -> str:
with _connect() as conn:
row = conn.execute("PRAGMA integrity_check").fetchone()