Add site banner, build number, and changelog
This commit is contained in:
@@ -77,6 +77,11 @@ SETTING_KEYS: List[str] = [
|
||||
"requests_cleanup_time",
|
||||
"requests_cleanup_days",
|
||||
"requests_data_source",
|
||||
"site_build_number",
|
||||
"site_banner_enabled",
|
||||
"site_banner_message",
|
||||
"site_banner_tone",
|
||||
"site_changelog",
|
||||
]
|
||||
|
||||
def _normalize_root_folders(folders: Any) -> List[Dict[str, Any]]:
|
||||
|
||||
@@ -1031,6 +1031,57 @@ def _normalize_indexer_name(value: Optional[str]) -> str:
|
||||
return "".join(ch for ch in value.lower().strip() if ch.isalnum())
|
||||
|
||||
|
||||
def _log_arr_http_error(service_label: str, action: str, exc: httpx.HTTPStatusError) -> None:
|
||||
if exc.response is None:
|
||||
logger.warning("%s %s failed: %s", service_label, action, exc)
|
||||
return
|
||||
status = exc.response.status_code
|
||||
body = exc.response.text
|
||||
if isinstance(body, str):
|
||||
body = body.strip()
|
||||
if len(body) > 800:
|
||||
body = f"{body[:800]}...(truncated)"
|
||||
logger.warning("%s %s failed: status=%s body=%s", service_label, action, status, body)
|
||||
|
||||
|
||||
def _format_rejections(rejections: Any) -> Optional[str]:
|
||||
if isinstance(rejections, str):
|
||||
return rejections.strip() or None
|
||||
if isinstance(rejections, list):
|
||||
reasons = []
|
||||
for item in rejections:
|
||||
reason = None
|
||||
if isinstance(item, dict):
|
||||
reason = (
|
||||
item.get("reason")
|
||||
or item.get("message")
|
||||
or item.get("errorMessage")
|
||||
)
|
||||
if not reason and item is not None:
|
||||
reason = str(item)
|
||||
if isinstance(reason, str) and reason.strip():
|
||||
reasons.append(reason.strip())
|
||||
if reasons:
|
||||
return "; ".join(reasons)
|
||||
return None
|
||||
|
||||
|
||||
def _release_push_accepted(response: Any) -> tuple[bool, Optional[str]]:
|
||||
if not isinstance(response, dict):
|
||||
return True, None
|
||||
rejections = response.get("rejections") or response.get("rejectionReasons")
|
||||
reason = _format_rejections(rejections)
|
||||
if reason:
|
||||
return False, reason
|
||||
if response.get("rejected") is True:
|
||||
return False, "rejected"
|
||||
if response.get("downloadAllowed") is False:
|
||||
return False, "download not allowed"
|
||||
if response.get("approved") is False:
|
||||
return False, "not approved"
|
||||
return True, None
|
||||
|
||||
|
||||
def _resolve_arr_indexer_id(
|
||||
indexers: Any, indexer_name: Optional[str], indexer_id: Optional[int], service_label: str
|
||||
) -> Optional[int]:
|
||||
@@ -1745,122 +1796,22 @@ async def action_grab(
|
||||
bool(release_title),
|
||||
)
|
||||
|
||||
push_payload = None
|
||||
if download_url and release_title:
|
||||
push_payload = {
|
||||
"title": release_title,
|
||||
"downloadUrl": download_url,
|
||||
"protocol": release_protocol,
|
||||
"publishDate": release_publish,
|
||||
"size": release_size,
|
||||
"indexer": indexer_name,
|
||||
"guid": guid,
|
||||
"seeders": release_seeders,
|
||||
"leechers": release_leechers,
|
||||
}
|
||||
|
||||
runtime = get_runtime_settings()
|
||||
if not download_url:
|
||||
raise HTTPException(status_code=400, detail="Missing downloadUrl")
|
||||
if snapshot.request_type.value == "tv":
|
||||
client = SonarrClient(runtime.sonarr_base_url, runtime.sonarr_api_key)
|
||||
if not client.configured():
|
||||
raise HTTPException(status_code=400, detail="Sonarr not configured")
|
||||
try:
|
||||
indexers = await client.get_indexers()
|
||||
resolved_indexer_id = _resolve_arr_indexer_id(indexers, indexer_name, indexer_id, "Sonarr")
|
||||
response = None
|
||||
action_message = "Grab sent to Sonarr."
|
||||
if resolved_indexer_id is not None:
|
||||
indexer_id = resolved_indexer_id
|
||||
logger.info("Sonarr grab: attempting DownloadRelease command.")
|
||||
try:
|
||||
response = await client.download_release(str(guid), int(indexer_id))
|
||||
except httpx.HTTPStatusError as exc:
|
||||
if exc.response is not None and exc.response.status_code in {404, 405}:
|
||||
logger.info("Sonarr grab: DownloadRelease unsupported; will try release push.")
|
||||
response = None
|
||||
else:
|
||||
raise
|
||||
if response is None and push_payload:
|
||||
logger.info("Sonarr grab: attempting release push.")
|
||||
try:
|
||||
response = await client.push_release(push_payload)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
if exc.response is not None and exc.response.status_code == 404:
|
||||
logger.info("Sonarr grab: release push not supported.")
|
||||
else:
|
||||
raise
|
||||
if response is None:
|
||||
category = _resolve_qbittorrent_category(
|
||||
runtime.sonarr_qbittorrent_category, "sonarr"
|
||||
)
|
||||
qbittorrent_added = await _fallback_qbittorrent_download(download_url, category)
|
||||
if qbittorrent_added:
|
||||
action_message = f"Grab sent to qBittorrent (category {category})."
|
||||
response = {"qbittorrent": "queued"}
|
||||
else:
|
||||
if resolved_indexer_id is None:
|
||||
detail = "Indexer not found in Sonarr and no release push available."
|
||||
elif not push_payload:
|
||||
detail = "Sonarr did not accept the grab request (no release URL available)."
|
||||
else:
|
||||
detail = "Sonarr did not accept the grab request (DownloadRelease unsupported)."
|
||||
raise HTTPException(status_code=400, detail=detail)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
raise HTTPException(status_code=502, detail=f"Sonarr grab failed: {exc}") from exc
|
||||
await asyncio.to_thread(
|
||||
save_action, request_id, "grab", "Grab release", "ok", action_message
|
||||
)
|
||||
return {"status": "ok", "response": response}
|
||||
category = _resolve_qbittorrent_category(runtime.sonarr_qbittorrent_category, "sonarr")
|
||||
if snapshot.request_type.value == "movie":
|
||||
client = RadarrClient(runtime.radarr_base_url, runtime.radarr_api_key)
|
||||
if not client.configured():
|
||||
raise HTTPException(status_code=400, detail="Radarr not configured")
|
||||
try:
|
||||
indexers = await client.get_indexers()
|
||||
resolved_indexer_id = _resolve_arr_indexer_id(indexers, indexer_name, indexer_id, "Radarr")
|
||||
response = None
|
||||
action_message = "Grab sent to Radarr."
|
||||
if resolved_indexer_id is not None:
|
||||
indexer_id = resolved_indexer_id
|
||||
logger.info("Radarr grab: attempting DownloadRelease command.")
|
||||
try:
|
||||
response = await client.download_release(str(guid), int(indexer_id))
|
||||
except httpx.HTTPStatusError as exc:
|
||||
if exc.response is not None and exc.response.status_code in {404, 405}:
|
||||
logger.info("Radarr grab: DownloadRelease unsupported; will try release push.")
|
||||
response = None
|
||||
else:
|
||||
raise
|
||||
if response is None and push_payload:
|
||||
logger.info("Radarr grab: attempting release push.")
|
||||
try:
|
||||
response = await client.push_release(push_payload)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
if exc.response is not None and exc.response.status_code == 404:
|
||||
logger.info("Radarr grab: release push not supported.")
|
||||
else:
|
||||
raise
|
||||
if response is None:
|
||||
category = _resolve_qbittorrent_category(
|
||||
runtime.radarr_qbittorrent_category, "radarr"
|
||||
)
|
||||
qbittorrent_added = await _fallback_qbittorrent_download(download_url, category)
|
||||
if qbittorrent_added:
|
||||
action_message = f"Grab sent to qBittorrent (category {category})."
|
||||
response = {"qbittorrent": "queued"}
|
||||
else:
|
||||
if resolved_indexer_id is None:
|
||||
detail = "Indexer not found in Radarr and no release push available."
|
||||
elif not push_payload:
|
||||
detail = "Radarr did not accept the grab request (no release URL available)."
|
||||
else:
|
||||
detail = "Radarr did not accept the grab request (DownloadRelease unsupported)."
|
||||
raise HTTPException(status_code=400, detail=detail)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
raise HTTPException(status_code=502, detail=f"Radarr grab failed: {exc}") from exc
|
||||
await asyncio.to_thread(
|
||||
save_action, request_id, "grab", "Grab release", "ok", action_message
|
||||
)
|
||||
return {"status": "ok", "response": response}
|
||||
category = _resolve_qbittorrent_category(runtime.radarr_qbittorrent_category, "radarr")
|
||||
if snapshot.request_type.value not in {"tv", "movie"}:
|
||||
raise HTTPException(status_code=400, detail="Unknown request type")
|
||||
|
||||
qbittorrent_added = await _fallback_qbittorrent_download(download_url, category)
|
||||
if not qbittorrent_added:
|
||||
raise HTTPException(status_code=400, detail="Failed to add torrent to qBittorrent")
|
||||
action_message = f"Grab sent to qBittorrent (category {category})."
|
||||
await asyncio.to_thread(
|
||||
save_action, request_id, "grab", "Grab release", "ok", action_message
|
||||
)
|
||||
return {"status": "ok", "response": {"qbittorrent": "queued"}}
|
||||
|
||||
raise HTTPException(status_code=400, detail="Unknown request type")
|
||||
|
||||
39
backend/app/routers/site.py
Normal file
39
backend/app/routers/site.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from ..auth import get_current_user
|
||||
from ..runtime import get_runtime_settings
|
||||
|
||||
router = APIRouter(prefix="/site", tags=["site"])
|
||||
|
||||
_BANNER_TONES = {"info", "warning", "error", "maintenance"}
|
||||
|
||||
|
||||
def _build_site_info(include_changelog: bool) -> Dict[str, Any]:
|
||||
runtime = get_runtime_settings()
|
||||
banner_message = (runtime.site_banner_message or "").strip()
|
||||
tone = (runtime.site_banner_tone or "info").strip().lower()
|
||||
if tone not in _BANNER_TONES:
|
||||
tone = "info"
|
||||
info = {
|
||||
"buildNumber": (runtime.site_build_number or "").strip(),
|
||||
"banner": {
|
||||
"enabled": bool(runtime.site_banner_enabled and banner_message),
|
||||
"message": banner_message,
|
||||
"tone": tone,
|
||||
},
|
||||
}
|
||||
if include_changelog:
|
||||
info["changelog"] = (runtime.site_changelog or "").strip()
|
||||
return info
|
||||
|
||||
|
||||
@router.get("/public")
|
||||
async def site_public() -> Dict[str, Any]:
|
||||
return _build_site_info(False)
|
||||
|
||||
|
||||
@router.get("/info")
|
||||
async def site_info(user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
|
||||
return _build_site_info(True)
|
||||
Reference in New Issue
Block a user