Persist Seerr media failure suppression and reduce sync error noise
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user