Finalize diagnostics, logging controls, and email test support
This commit is contained in:
@@ -84,9 +84,11 @@ from ..services.invite_email import (
|
||||
get_invite_email_templates,
|
||||
reset_invite_email_template,
|
||||
save_invite_email_template,
|
||||
send_test_email,
|
||||
send_templated_email,
|
||||
smtp_email_config_ready,
|
||||
)
|
||||
from ..services.diagnostics import get_diagnostics_catalog, run_diagnostics
|
||||
import logging
|
||||
from ..logging_config import configure_logging
|
||||
from ..routers import requests as requests_router
|
||||
@@ -192,6 +194,10 @@ SETTING_KEYS: List[str] = [
|
||||
"qbittorrent_password",
|
||||
"log_level",
|
||||
"log_file",
|
||||
"log_file_max_bytes",
|
||||
"log_file_backup_count",
|
||||
"log_http_client_level",
|
||||
"log_background_sync_level",
|
||||
"requests_sync_ttl_minutes",
|
||||
"requests_poll_interval_seconds",
|
||||
"requests_delta_sync_interval_minutes",
|
||||
@@ -609,6 +615,7 @@ async def list_settings() -> Dict[str, Any]:
|
||||
async def update_settings(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
updates = 0
|
||||
touched_logging = False
|
||||
changed_keys: List[str] = []
|
||||
for key, value in payload.items():
|
||||
if key not in SETTING_KEYS:
|
||||
raise HTTPException(status_code=400, detail=f"Unknown setting: {key}")
|
||||
@@ -617,6 +624,7 @@ async def update_settings(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if isinstance(value, str) and value.strip() == "":
|
||||
delete_setting(key)
|
||||
updates += 1
|
||||
changed_keys.append(key)
|
||||
continue
|
||||
value_to_store = str(value).strip() if isinstance(value, str) else str(value)
|
||||
if key in URL_SETTING_KEYS and value_to_store:
|
||||
@@ -627,14 +635,79 @@ async def update_settings(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
raise HTTPException(status_code=400, detail=f"{friendly_key}: {exc}") from exc
|
||||
set_setting(key, value_to_store)
|
||||
updates += 1
|
||||
if key in {"log_level", "log_file"}:
|
||||
changed_keys.append(key)
|
||||
if key in {"log_level", "log_file", "log_file_max_bytes", "log_file_backup_count", "log_http_client_level", "log_background_sync_level"}:
|
||||
touched_logging = True
|
||||
if touched_logging:
|
||||
runtime = get_runtime_settings()
|
||||
configure_logging(runtime.log_level, runtime.log_file)
|
||||
configure_logging(
|
||||
runtime.log_level,
|
||||
runtime.log_file,
|
||||
log_file_max_bytes=runtime.log_file_max_bytes,
|
||||
log_file_backup_count=runtime.log_file_backup_count,
|
||||
log_http_client_level=runtime.log_http_client_level,
|
||||
log_background_sync_level=runtime.log_background_sync_level,
|
||||
)
|
||||
logger.info("Admin updated settings: count=%s keys=%s", updates, changed_keys)
|
||||
return {"status": "ok", "updated": updates}
|
||||
|
||||
|
||||
@router.post("/settings/test/email")
|
||||
async def test_email_settings(request: Request) -> Dict[str, Any]:
|
||||
recipient_email = None
|
||||
content_type = (request.headers.get("content-type") or "").split(";", 1)[0].strip().lower()
|
||||
try:
|
||||
if content_type == "application/json":
|
||||
payload = await request.json()
|
||||
if isinstance(payload, dict) and isinstance(payload.get("recipient_email"), str):
|
||||
recipient_email = payload["recipient_email"]
|
||||
elif content_type in {
|
||||
"application/x-www-form-urlencoded",
|
||||
"multipart/form-data",
|
||||
}:
|
||||
form = await request.form()
|
||||
candidate = form.get("recipient_email")
|
||||
if isinstance(candidate, str):
|
||||
recipient_email = candidate
|
||||
except Exception:
|
||||
recipient_email = None
|
||||
try:
|
||||
result = await send_test_email(recipient_email=recipient_email)
|
||||
except RuntimeError as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
logger.info("Admin triggered SMTP test: recipient=%s", result.get("recipient_email"))
|
||||
return {"status": "ok", **result}
|
||||
|
||||
|
||||
@router.get("/diagnostics")
|
||||
async def diagnostics_catalog() -> Dict[str, Any]:
|
||||
return {"status": "ok", **get_diagnostics_catalog()}
|
||||
|
||||
|
||||
@router.post("/diagnostics/run")
|
||||
async def diagnostics_run(payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
keys: Optional[List[str]] = None
|
||||
recipient_email: Optional[str] = None
|
||||
if payload is not None:
|
||||
raw_keys = payload.get("keys")
|
||||
if raw_keys is not None:
|
||||
if not isinstance(raw_keys, list):
|
||||
raise HTTPException(status_code=400, detail="keys must be an array of diagnostic keys")
|
||||
keys = []
|
||||
for raw_key in raw_keys:
|
||||
if not isinstance(raw_key, str):
|
||||
raise HTTPException(status_code=400, detail="Each diagnostic key must be a string")
|
||||
normalized = raw_key.strip()
|
||||
if normalized:
|
||||
keys.append(normalized)
|
||||
raw_recipient_email = payload.get("recipient_email")
|
||||
if raw_recipient_email is not None:
|
||||
if not isinstance(raw_recipient_email, str):
|
||||
raise HTTPException(status_code=400, detail="recipient_email must be a string")
|
||||
recipient_email = raw_recipient_email.strip() or None
|
||||
return {"status": "ok", **(await run_diagnostics(keys, recipient_email=recipient_email))}
|
||||
|
||||
|
||||
@router.get("/sonarr/options")
|
||||
async def sonarr_options() -> Dict[str, Any]:
|
||||
runtime = get_runtime_settings()
|
||||
@@ -1061,12 +1134,14 @@ async def get_user_summary_by_id(user_id: int) -> Dict[str, Any]:
|
||||
@router.post("/users/{username}/block")
|
||||
async def block_user(username: str) -> Dict[str, Any]:
|
||||
set_user_blocked(username, True)
|
||||
logger.warning("Admin blocked user: username=%s", username)
|
||||
return {"status": "ok", "username": username, "blocked": True}
|
||||
|
||||
|
||||
@router.post("/users/{username}/unblock")
|
||||
async def unblock_user(username: str) -> Dict[str, Any]:
|
||||
set_user_blocked(username, False)
|
||||
logger.info("Admin unblocked user: username=%s", username)
|
||||
return {"status": "ok", "username": username, "blocked": False}
|
||||
|
||||
|
||||
@@ -1173,6 +1248,17 @@ async def user_system_action(username: str, payload: Dict[str, Any]) -> Dict[str
|
||||
for system in (result.get("jellyfin"), result.get("jellyseerr"), result.get("email"))
|
||||
):
|
||||
result["status"] = "partial"
|
||||
logger.info(
|
||||
"Admin system action completed: username=%s action=%s overall=%s local=%s jellyfin=%s jellyseerr=%s invites=%s email=%s",
|
||||
username,
|
||||
action,
|
||||
result.get("status"),
|
||||
result.get("local", {}).get("status"),
|
||||
result.get("jellyfin", {}).get("status"),
|
||||
result.get("jellyseerr", {}).get("status"),
|
||||
result.get("invites", {}).get("status"),
|
||||
result.get("email", {}).get("status"),
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@@ -1450,6 +1536,15 @@ async def create_profile(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
)
|
||||
except sqlite3.IntegrityError as exc:
|
||||
raise HTTPException(status_code=409, detail="A profile with that name already exists") from exc
|
||||
logger.info(
|
||||
"Admin created profile: profile_id=%s name=%s role=%s active=%s auto_search=%s expires_days=%s",
|
||||
profile.get("id"),
|
||||
profile.get("name"),
|
||||
profile.get("role"),
|
||||
profile.get("is_active"),
|
||||
profile.get("auto_search_enabled"),
|
||||
profile.get("account_expires_days"),
|
||||
)
|
||||
return {"status": "ok", "profile": profile}
|
||||
|
||||
|
||||
@@ -1487,6 +1582,15 @@ async def edit_profile(profile_id: int, payload: Dict[str, Any]) -> Dict[str, An
|
||||
raise HTTPException(status_code=409, detail="A profile with that name already exists") from exc
|
||||
if not profile:
|
||||
raise HTTPException(status_code=404, detail="Profile not found")
|
||||
logger.info(
|
||||
"Admin updated profile: profile_id=%s name=%s role=%s active=%s auto_search=%s expires_days=%s",
|
||||
profile.get("id"),
|
||||
profile.get("name"),
|
||||
profile.get("role"),
|
||||
profile.get("is_active"),
|
||||
profile.get("auto_search_enabled"),
|
||||
profile.get("account_expires_days"),
|
||||
)
|
||||
return {"status": "ok", "profile": profile}
|
||||
|
||||
|
||||
@@ -1498,6 +1602,7 @@ async def remove_profile(profile_id: int) -> Dict[str, Any]:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
if not deleted:
|
||||
raise HTTPException(status_code=404, detail="Profile not found")
|
||||
logger.warning("Admin deleted profile: profile_id=%s", profile_id)
|
||||
return {"status": "ok", "deleted": True, "profile_id": profile_id}
|
||||
|
||||
|
||||
@@ -1561,6 +1666,7 @@ async def update_invite_policy(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
master_invite_value = payload.get("master_invite_id")
|
||||
if master_invite_value in (None, "", 0, "0"):
|
||||
set_setting(SELF_SERVICE_INVITE_MASTER_ID_KEY, None)
|
||||
logger.info("Admin cleared invite policy master invite")
|
||||
return {"status": "ok", "policy": {"master_invite_id": None, "master_invite": None}}
|
||||
try:
|
||||
master_invite_id = int(master_invite_value)
|
||||
@@ -1572,6 +1678,7 @@ async def update_invite_policy(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if not invite:
|
||||
raise HTTPException(status_code=404, detail="Master invite not found")
|
||||
set_setting(SELF_SERVICE_INVITE_MASTER_ID_KEY, str(master_invite_id))
|
||||
logger.info("Admin updated invite policy: master_invite_id=%s", master_invite_id)
|
||||
return {
|
||||
"status": "ok",
|
||||
"policy": {
|
||||
@@ -1613,6 +1720,7 @@ async def update_invite_email_template_settings(template_key: str, payload: Dict
|
||||
body_text=body_text or "",
|
||||
body_html=body_html or "",
|
||||
)
|
||||
logger.info("Admin updated invite email template: template=%s", template_key)
|
||||
return {"status": "ok", "template": template}
|
||||
|
||||
|
||||
@@ -1621,6 +1729,7 @@ async def reset_invite_email_template_settings(template_key: str) -> Dict[str, A
|
||||
if template_key not in INVITE_EMAIL_TEMPLATE_KEYS:
|
||||
raise HTTPException(status_code=404, detail="Email template not found")
|
||||
template = reset_invite_email_template(template_key)
|
||||
logger.info("Admin reset invite email template: template=%s", template_key)
|
||||
return {"status": "ok", "template": template}
|
||||
|
||||
|
||||
@@ -1666,6 +1775,13 @@ async def send_invite_email(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
logger.info(
|
||||
"Admin sent invite email template: template=%s recipient=%s invite_id=%s username=%s",
|
||||
template_key,
|
||||
result.get("recipient_email"),
|
||||
invite.get("id") if invite else None,
|
||||
user.get("username") if user else None,
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
@@ -1725,6 +1841,18 @@ async def create_invite(payload: Dict[str, Any], current_user: Dict[str, Any] =
|
||||
)
|
||||
except Exception as exc:
|
||||
email_error = str(exc)
|
||||
logger.info(
|
||||
"Admin created invite: invite_id=%s code=%s label=%s profile_id=%s role=%s max_uses=%s enabled=%s recipient_email=%s send_email=%s",
|
||||
invite.get("id"),
|
||||
invite.get("code"),
|
||||
invite.get("label"),
|
||||
invite.get("profile_id"),
|
||||
invite.get("role"),
|
||||
invite.get("max_uses"),
|
||||
invite.get("enabled"),
|
||||
invite.get("recipient_email"),
|
||||
send_email,
|
||||
)
|
||||
return {
|
||||
"status": "partial" if email_error else "ok",
|
||||
"invite": invite,
|
||||
@@ -1785,6 +1913,18 @@ async def edit_invite(invite_id: int, payload: Dict[str, Any]) -> Dict[str, Any]
|
||||
)
|
||||
except Exception as exc:
|
||||
email_error = str(exc)
|
||||
logger.info(
|
||||
"Admin updated invite: invite_id=%s code=%s label=%s profile_id=%s role=%s max_uses=%s enabled=%s recipient_email=%s send_email=%s",
|
||||
invite.get("id"),
|
||||
invite.get("code"),
|
||||
invite.get("label"),
|
||||
invite.get("profile_id"),
|
||||
invite.get("role"),
|
||||
invite.get("max_uses"),
|
||||
invite.get("enabled"),
|
||||
invite.get("recipient_email"),
|
||||
send_email,
|
||||
)
|
||||
return {
|
||||
"status": "partial" if email_error else "ok",
|
||||
"invite": invite,
|
||||
@@ -1803,4 +1943,5 @@ async def remove_invite(invite_id: int) -> Dict[str, Any]:
|
||||
deleted = delete_signup_invite(invite_id)
|
||||
if not deleted:
|
||||
raise HTTPException(status_code=404, detail="Invite not found")
|
||||
logger.warning("Admin deleted invite: invite_id=%s", invite_id)
|
||||
return {"status": "ok", "deleted": True, "invite_id": invite_id}
|
||||
|
||||
@@ -115,6 +115,7 @@ def _record_login_failure(request: Request, username: str) -> None:
|
||||
_prune_attempts(user_bucket, now, window)
|
||||
ip_bucket.append(now)
|
||||
user_bucket.append(now)
|
||||
logger.warning("login failure recorded username=%s client=%s", user_key, ip_key)
|
||||
|
||||
|
||||
def _clear_login_failures(request: Request, username: str) -> None:
|
||||
@@ -148,6 +149,12 @@ def _enforce_login_rate_limit(request: Request, username: str) -> None:
|
||||
if retry_candidates:
|
||||
retry_after = max(retry_candidates)
|
||||
if exceeded:
|
||||
logger.warning(
|
||||
"login rate limit exceeded username=%s client=%s retry_after=%s",
|
||||
user_key,
|
||||
ip_key,
|
||||
retry_after,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
||||
detail="Too many login attempts. Try again shortly.",
|
||||
@@ -474,6 +481,11 @@ def _master_invite_controlled_values(master_invite: dict) -> tuple[int | None, s
|
||||
@router.post("/login")
|
||||
async def login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()) -> dict:
|
||||
_enforce_login_rate_limit(request, form_data.username)
|
||||
logger.info(
|
||||
"login attempt provider=local username=%s client=%s",
|
||||
_login_rate_key_user(form_data.username),
|
||||
_auth_client_ip(request),
|
||||
)
|
||||
# Provider placeholder passwords must never be accepted by the local-login endpoint.
|
||||
if form_data.password in {"jellyfin-user", "jellyseerr-user"}:
|
||||
_record_login_failure(request, form_data.username)
|
||||
@@ -483,6 +495,11 @@ async def login(request: Request, form_data: OAuth2PasswordRequestForm = Depends
|
||||
str(user.get("auth_provider") or "local").lower() != "local" for user in matching_users
|
||||
)
|
||||
if has_external_match:
|
||||
logger.warning(
|
||||
"login rejected provider=local username=%s reason=external-account client=%s",
|
||||
_login_rate_key_user(form_data.username),
|
||||
_auth_client_ip(request),
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="This account uses external sign-in. Use the external sign-in option.",
|
||||
@@ -492,6 +509,11 @@ async def login(request: Request, form_data: OAuth2PasswordRequestForm = Depends
|
||||
_record_login_failure(request, form_data.username)
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
|
||||
if user.get("auth_provider") != "local":
|
||||
logger.warning(
|
||||
"login rejected provider=local username=%s reason=wrong-provider client=%s",
|
||||
_login_rate_key_user(form_data.username),
|
||||
_auth_client_ip(request),
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="This account uses external sign-in. Use the external sign-in option.",
|
||||
@@ -500,6 +522,12 @@ async def login(request: Request, form_data: OAuth2PasswordRequestForm = Depends
|
||||
token = create_access_token(user["username"], user["role"])
|
||||
_clear_login_failures(request, form_data.username)
|
||||
set_last_login(user["username"])
|
||||
logger.info(
|
||||
"login success provider=local username=%s role=%s client=%s",
|
||||
user["username"],
|
||||
user["role"],
|
||||
_auth_client_ip(request),
|
||||
)
|
||||
return {
|
||||
"access_token": token,
|
||||
"token_type": "bearer",
|
||||
@@ -510,6 +538,11 @@ async def login(request: Request, form_data: OAuth2PasswordRequestForm = Depends
|
||||
@router.post("/jellyfin/login")
|
||||
async def jellyfin_login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()) -> dict:
|
||||
_enforce_login_rate_limit(request, form_data.username)
|
||||
logger.info(
|
||||
"login attempt provider=jellyfin username=%s client=%s",
|
||||
_login_rate_key_user(form_data.username),
|
||||
_auth_client_ip(request),
|
||||
)
|
||||
runtime = get_runtime_settings()
|
||||
client = JellyfinClient(runtime.jellyfin_base_url, runtime.jellyfin_api_key)
|
||||
if not client.configured():
|
||||
@@ -527,6 +560,11 @@ async def jellyfin_login(request: Request, form_data: OAuth2PasswordRequestForm
|
||||
token = create_access_token(canonical_username, "user")
|
||||
_clear_login_failures(request, username)
|
||||
set_last_login(canonical_username)
|
||||
logger.info(
|
||||
"login success provider=jellyfin username=%s source=cache client=%s",
|
||||
canonical_username,
|
||||
_auth_client_ip(request),
|
||||
)
|
||||
return {
|
||||
"access_token": token,
|
||||
"token_type": "bearer",
|
||||
@@ -535,6 +573,11 @@ async def jellyfin_login(request: Request, form_data: OAuth2PasswordRequestForm
|
||||
try:
|
||||
response = await client.authenticate_by_name(username, password)
|
||||
except Exception as exc:
|
||||
logger.exception(
|
||||
"login upstream error provider=jellyfin username=%s client=%s",
|
||||
_login_rate_key_user(username),
|
||||
_auth_client_ip(request),
|
||||
)
|
||||
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc
|
||||
if not isinstance(response, dict) or not response.get("User"):
|
||||
_record_login_failure(request, username)
|
||||
@@ -564,6 +607,12 @@ async def jellyfin_login(request: Request, form_data: OAuth2PasswordRequestForm
|
||||
token = create_access_token(canonical_username, "user")
|
||||
_clear_login_failures(request, username)
|
||||
set_last_login(canonical_username)
|
||||
logger.info(
|
||||
"login success provider=jellyfin username=%s linked_seerr_id=%s client=%s",
|
||||
canonical_username,
|
||||
get_user_by_username(canonical_username).get("jellyseerr_user_id") if get_user_by_username(canonical_username) else None,
|
||||
_auth_client_ip(request),
|
||||
)
|
||||
return {
|
||||
"access_token": token,
|
||||
"token_type": "bearer",
|
||||
@@ -575,6 +624,11 @@ async def jellyfin_login(request: Request, form_data: OAuth2PasswordRequestForm
|
||||
@router.post("/jellyseerr/login")
|
||||
async def jellyseerr_login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()) -> dict:
|
||||
_enforce_login_rate_limit(request, form_data.username)
|
||||
logger.info(
|
||||
"login attempt provider=seerr username=%s client=%s",
|
||||
_login_rate_key_user(form_data.username),
|
||||
_auth_client_ip(request),
|
||||
)
|
||||
runtime = get_runtime_settings()
|
||||
client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key)
|
||||
if not client.configured():
|
||||
@@ -582,6 +636,11 @@ async def jellyseerr_login(request: Request, form_data: OAuth2PasswordRequestFor
|
||||
try:
|
||||
response = await client.login_local(form_data.username, form_data.password)
|
||||
except Exception as exc:
|
||||
logger.exception(
|
||||
"login upstream error provider=seerr username=%s client=%s",
|
||||
_login_rate_key_user(form_data.username),
|
||||
_auth_client_ip(request),
|
||||
)
|
||||
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc
|
||||
if not isinstance(response, dict):
|
||||
_record_login_failure(request, form_data.username)
|
||||
@@ -605,6 +664,12 @@ async def jellyseerr_login(request: Request, form_data: OAuth2PasswordRequestFor
|
||||
token = create_access_token(canonical_username, "user")
|
||||
_clear_login_failures(request, form_data.username)
|
||||
set_last_login(canonical_username)
|
||||
logger.info(
|
||||
"login success provider=seerr username=%s seerr_user_id=%s client=%s",
|
||||
canonical_username,
|
||||
jellyseerr_user_id,
|
||||
_auth_client_ip(request),
|
||||
)
|
||||
return {
|
||||
"access_token": token,
|
||||
"token_type": "bearer",
|
||||
@@ -663,6 +728,11 @@ async def signup(payload: dict) -> dict:
|
||||
)
|
||||
if get_user_by_username(username):
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="User already exists")
|
||||
logger.info(
|
||||
"signup attempt username=%s invite_code=%s",
|
||||
username,
|
||||
invite_code,
|
||||
)
|
||||
|
||||
invite = get_signup_invite_by_code(invite_code)
|
||||
if not invite:
|
||||
@@ -709,6 +779,7 @@ async def signup(payload: dict) -> dict:
|
||||
|
||||
jellyfin_client = JellyfinClient(runtime.jellyfin_base_url, runtime.jellyfin_api_key)
|
||||
if jellyfin_client.configured():
|
||||
logger.info("signup provisioning jellyfin username=%s", username)
|
||||
auth_provider = "jellyfin"
|
||||
local_password_value = "jellyfin-user"
|
||||
try:
|
||||
@@ -788,6 +859,14 @@ async def signup(payload: dict) -> dict:
|
||||
_assert_user_can_login(created_user)
|
||||
token = create_access_token(username, role)
|
||||
set_last_login(username)
|
||||
logger.info(
|
||||
"signup success username=%s role=%s auth_provider=%s profile_id=%s invite_code=%s",
|
||||
username,
|
||||
role,
|
||||
created_user.get("auth_provider") if created_user else auth_provider,
|
||||
created_user.get("profile_id") if created_user else None,
|
||||
invite.get("code"),
|
||||
)
|
||||
return {
|
||||
"access_token": token,
|
||||
"token_type": "bearer",
|
||||
|
||||
@@ -596,7 +596,7 @@ async def _sync_all_requests(client: JellyseerrClient) -> int:
|
||||
skip += take
|
||||
_sync_state["skip"] = skip
|
||||
_sync_state["message"] = f"Synced {stored} requests"
|
||||
logger.info("Seerr sync progress: stored=%s skip=%s", stored, skip)
|
||||
logger.debug("Seerr sync progress: stored=%s skip=%s", stored, skip)
|
||||
_sync_state.update(
|
||||
{
|
||||
"status": "completed",
|
||||
@@ -719,7 +719,7 @@ async def _sync_delta_requests(client: JellyseerrClient) -> int:
|
||||
skip += take
|
||||
_sync_state["skip"] = skip
|
||||
_sync_state["message"] = f"Delta synced {stored} requests"
|
||||
logger.info("Seerr delta sync progress: stored=%s skip=%s", stored, skip)
|
||||
logger.debug("Seerr delta sync progress: stored=%s skip=%s", stored, skip)
|
||||
deduped = prune_duplicate_requests_cache()
|
||||
if deduped:
|
||||
logger.info("Seerr delta sync removed duplicate rows: %s", deduped)
|
||||
|
||||
Reference in New Issue
Block a user