simplifications and refactorings

This commit is contained in:
2026-05-04 16:59:06 +02:00
parent e14cb8a654
commit 4cdb4b144c
18 changed files with 30 additions and 456 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
# Backend - Media Library Viewer API
# Manage Backend API
FastAPI backend serving the REST API for Jellyfin media browsing, SSH file inspection, server monitoring, and JWT-protected access.
FastAPI backend serving the REST API for Jellyfin media browsing, SSH file inspection, server monitoring, and JWT-protected access for Manage.
## Project structure
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "media-library-viewer-backend"
version = "0.1.0"
description = "FastAPI backend for Media Library Viewer"
description = "FastAPI backend for Manage"
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.100",
@@ -46,7 +46,7 @@ class Settings(BaseSettings):
smtp_username: str = ""
smtp_password: str = ""
smtp_from_address: str = ""
smtp_from_name: str = "Media Library Viewer"
smtp_from_name: str = "Manage"
smtp_use_tls: bool = True
smtp_use_ssl: bool = False
smtp_timeout: int = 30
+2 -2
View File
@@ -34,9 +34,9 @@ async def lifespan(app: FastAPI):
app = FastAPI(
title="Media Library Viewer API",
title="Manage API",
version="0.1.0",
description="Backend API for Jellyfin media browsing, SSH file inspection, server monitoring, and JWT-protected access.",
description="Manage API for Jellyfin media browsing, SSH file inspection, server monitoring, and JWT-protected access.",
lifespan=lifespan,
)
@@ -16,7 +16,7 @@ from media_library_viewer_api.dependencies import (
get_jellyseerr_client,
get_mail_queue,
)
from media_library_viewer_api.services.mailer import EmailAttachment, test_smtp_connection, validate_smtp_settings
from media_library_viewer_api.services.mailer import EmailAttachment, validate_smtp_settings
logger = logging.getLogger(__name__)
@@ -287,13 +287,6 @@ def get_user_message_status(mail_queue=Depends(get_mail_queue)) -> dict[str, Any
return mail_queue.status()
@router.post("/message/test-smtp")
def test_user_message_smtp() -> dict[str, Any]:
"""Test the configured SMTP connection without sending an email."""
settings = get_settings()
return test_smtp_connection(settings)
@router.post("/message", status_code=status.HTTP_202_ACCEPTED)
async def post_user_message(
recipient_ids: str = Form(...),
@@ -99,11 +99,6 @@ def _smtp_settings(settings: object) -> dict[str, object]:
}
def _smtp_mode_label(mode: dict[str, object]) -> str:
transport = "SSL" if mode["use_ssl"] else "STARTTLS" if mode["use_tls"] else "plain SMTP"
return f"{mode['smtp_host']}:{mode['smtp_port']} via {transport}"
def _smtp_mode_candidates(settings: object) -> list[dict[str, Any]]:
base = _smtp_settings(settings)
candidates = [dict(base, mode_label="configured")]
@@ -175,7 +170,7 @@ def _smtp_sender_not_authorized(error: Exception) -> bool:
def _smtp_attempt_metadata(mode: dict[str, Any]) -> dict[str, Any]:
transport = "SSL" if mode["use_ssl"] else "STARTTLS" if mode["use_tls"] else "plain SMTP"
return {
"label": str(mode.get("mode_label") or _smtp_mode_label(mode)),
"label": str(mode.get("mode_label") or f"{mode["smtp_host"]}:{mode["smtp_port"]}"),
"smtp_host": str(mode["smtp_host"]),
"smtp_port": int(mode["smtp_port"]),
"use_tls": bool(mode["use_tls"]),
@@ -224,127 +219,6 @@ def describe_smtp_error(error: Exception) -> str:
return f"SMTP delivery failed: {error}"
def test_smtp_connection(settings: object) -> dict[str, object]:
"""Validate SMTP connectivity and authentication without sending a message."""
try:
base = _smtp_settings(settings)
except ValueError as exc:
return {
"status": "error",
"message": str(exc),
"attempts": [],
"selected_mode": None,
"from_address": "",
"from_name": str(getattr(settings, "smtp_from_name", "") or "").strip() or "Media Library Viewer",
"smtp_host": str(getattr(settings, "smtp_host", "") or "").strip(),
"smtp_port": int(getattr(settings, "smtp_port", 587) or 587),
"use_tls": bool(getattr(settings, "smtp_use_tls", True)),
"use_ssl": bool(getattr(settings, "smtp_use_ssl", False)),
"authenticated": False,
}
from_address = _from_address(settings)
from_name = str(getattr(settings, "smtp_from_name", "") or "").strip() or "Media Library Viewer"
attempts: list[dict[str, Any]] = []
last_error = ""
logger.info(
"SMTP test requested from_address=%s auth_user=%s",
from_address,
base["smtp_username"] or "<none>",
)
for mode in _smtp_mode_candidates(settings):
meta = _smtp_attempt_metadata(mode)
logger.info(
"SMTP test attempting label=%s host=%s port=%s transport=%s auth_user=%s",
meta["label"],
meta["smtp_host"],
meta["smtp_port"],
meta["transport"],
meta["auth_user"],
)
try:
_probe_smtp_connection(mode)
attempts.append(
{
"label": meta["label"],
"smtp_host": meta["smtp_host"],
"smtp_port": meta["smtp_port"],
"use_tls": meta["use_tls"],
"use_ssl": meta["use_ssl"],
"status": "ok",
}
)
logger.info(
"SMTP test succeeded label=%s host=%s port=%s transport=%s auth_user=%s",
meta["label"],
meta["smtp_host"],
meta["smtp_port"],
meta["transport"],
meta["auth_user"],
)
return {
"status": "ok",
"message": f"SMTP connection successful using {meta['label']}",
"from_address": from_address,
"from_name": from_name,
"smtp_host": meta["smtp_host"],
"smtp_port": meta["smtp_port"],
"use_tls": meta["use_tls"],
"use_ssl": meta["use_ssl"],
"authenticated": bool(str(mode["smtp_username"])),
"selected_mode": {
"label": meta["label"],
"smtp_host": meta["smtp_host"],
"smtp_port": meta["smtp_port"],
"use_tls": meta["use_tls"],
"use_ssl": meta["use_ssl"],
},
"attempts": attempts,
}
except Exception as exc:
last_error = describe_smtp_error(exc)
attempts.append(
{
"label": meta["label"],
"smtp_host": meta["smtp_host"],
"smtp_port": meta["smtp_port"],
"use_tls": meta["use_tls"],
"use_ssl": meta["use_ssl"],
"status": "failed",
"error": last_error,
}
)
logger.warning(
"SMTP test failed label=%s host=%s port=%s transport=%s auth_user=%s error=%s",
meta["label"],
meta["smtp_host"],
meta["smtp_port"],
meta["transport"],
meta["auth_user"],
last_error,
)
logger.error(
"SMTP test exhausted attempts=%s error=%s",
[attempt["label"] for attempt in attempts],
last_error or "SMTP test failed.",
)
return {
"status": "error",
"message": last_error or "SMTP test failed.",
"from_address": from_address,
"from_name": from_name,
"smtp_host": base["smtp_host"],
"smtp_port": base["smtp_port"],
"use_tls": base["use_tls"],
"use_ssl": base["use_ssl"],
"authenticated": bool(base["smtp_username"]),
"selected_mode": None,
"attempts": attempts,
}
def build_email_message(
settings: object,
recipients: list[str],
@@ -358,7 +232,7 @@ def build_email_message(
) -> tuple[EmailMessage, str]:
"""Build a MIME email message with HTML and attachments."""
from_address = sender_address or _from_address(settings)
from_name = str(getattr(settings, "smtp_from_name", "") or "").strip() or "Media Library Viewer"
from_name = str(getattr(settings, "smtp_from_name", "") or "").strip() or "Manage"
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = formataddr((from_name, from_address))
+5 -104
View File
@@ -132,8 +132,10 @@ def test_client(mock_jellyfin, mock_jellyseerr, mock_ssh):
app.dependency_overrides[get_jellyseerr_client] = lambda: mock_jellyseerr
app.dependency_overrides[get_ssh_client] = lambda: mock_ssh
app.dependency_overrides[get_user_id] = lambda: "user123"
client = TestClient(app)
yield client
auth_settings = SimpleNamespace(auth_enabled=False)
with patch("media_library_viewer_api.auth.get_settings", return_value=auth_settings):
client = TestClient(app)
yield client
app.dependency_overrides.clear()
@@ -245,107 +247,6 @@ class TestUsers:
assert response.json()["state"] == "idle"
assert response.json()["pending_count"] == 0
def test_users_message_test_smtp(self, test_client):
settings = SimpleNamespace(
smtp_host="smtp.fastmail.com",
smtp_port=587,
smtp_username="main@fastmail.com",
smtp_password="app-password",
smtp_from_address="alias@example.com",
smtp_from_name="Media Library Viewer",
smtp_use_tls=True,
smtp_use_ssl=False,
smtp_timeout=15,
)
app.dependency_overrides[get_mail_queue] = lambda: MagicMock()
try:
with patch("media_library_viewer_api.routers.users.get_settings", return_value=settings), patch(
"media_library_viewer_api.routers.users.test_smtp_connection",
return_value={
"status": "ok",
"message": "SMTP connection successful using Fastmail STARTTLS 587",
"from_address": "alias@example.com",
"from_name": "Media Library Viewer",
"smtp_host": "smtp.fastmail.com",
"smtp_port": 587,
"use_tls": True,
"use_ssl": False,
"authenticated": True,
"selected_mode": {
"label": "Fastmail STARTTLS 587",
"smtp_host": "smtp.fastmail.com",
"smtp_port": 587,
"use_tls": True,
"use_ssl": False,
},
"attempts": [
{
"label": "Fastmail STARTTLS 587",
"smtp_host": "smtp.fastmail.com",
"smtp_port": 587,
"use_tls": True,
"use_ssl": False,
"status": "ok",
}
],
},
):
response = test_client.post("/api/users/message/test-smtp")
finally:
app.dependency_overrides.pop(get_mail_queue, None)
assert response.status_code == 200
data = response.json()
assert data["status"] == "ok"
assert data["smtp_host"] == "smtp.fastmail.com"
assert data["selected_mode"]["label"] == "Fastmail STARTTLS 587"
def test_users_message_test_smtp_timeout_message(self, test_client):
settings = SimpleNamespace(
smtp_host="smtp.fastmail.com",
smtp_port=587,
smtp_username="main@fastmail.com",
smtp_password="app-password",
smtp_from_address="alias@example.com",
smtp_from_name="Media Library Viewer",
smtp_use_tls=True,
smtp_use_ssl=False,
smtp_timeout=15,
)
app.dependency_overrides[get_mail_queue] = lambda: MagicMock()
try:
with patch("media_library_viewer_api.routers.users.get_settings", return_value=settings), patch(
"media_library_viewer_api.routers.users.test_smtp_connection",
return_value={
"status": "error",
"message": "SMTP connection timed out while waiting for the server greeting. Check host, port, network access, and SMTP_TIMEOUT.",
"from_address": "alias@example.com",
"from_name": "Media Library Viewer",
"smtp_host": "smtp.fastmail.com",
"smtp_port": 587,
"use_tls": True,
"use_ssl": False,
"authenticated": True,
"selected_mode": None,
"attempts": [
{
"label": "configured",
"smtp_host": "smtp.fastmail.com",
"smtp_port": 587,
"use_tls": True,
"use_ssl": False,
"status": "failed",
"error": "SMTP connection timed out while waiting for the server greeting. Check host, port, network access, and SMTP_TIMEOUT.",
}
],
},
):
response = test_client.post("/api/users/message/test-smtp")
finally:
app.dependency_overrides.pop(get_mail_queue, None)
assert response.status_code == 200
assert response.json()["status"] == "error"
assert "timed out" in response.json()["message"].lower()
def test_users_message_is_queued(self, test_client):
mail_queue = MagicMock()
mail_queue.status.return_value = {
@@ -371,7 +272,7 @@ class TestUsers:
smtp_username="mailer@example.com",
smtp_password="secret",
smtp_from_address="mailer@example.com",
smtp_from_name="Media Library Viewer",
smtp_from_name="Manage",
smtp_use_tls=True,
smtp_use_ssl=False,
smtp_timeout=15,
+4 -66
View File
@@ -12,7 +12,6 @@ from media_library_viewer_api.services.mailer import (
describe_smtp_error,
html_to_text,
send_email_message,
test_smtp_connection as smtp_connection_probe,
)
@@ -41,7 +40,7 @@ class MailerTests(unittest.TestCase):
smtp_username="mailer@example.com",
smtp_password="secret",
smtp_from_address="mailer@example.com",
smtp_from_name="Media Library Viewer",
smtp_from_name="Manage",
smtp_use_tls=True,
smtp_use_ssl=False,
smtp_timeout=15,
@@ -67,70 +66,9 @@ class MailerTests(unittest.TestCase):
smtp.send_message.assert_called_once()
message = smtp.send_message.call_args.args[0]
self.assertEqual(message["Subject"], "Hello")
self.assertEqual(message["From"], "Media Library Viewer <mailer@example.com>")
self.assertEqual(message["From"], "Manage <mailer@example.com>")
self.assertEqual(result["recipient_count"], 2)
self.assertEqual(result["attachment_count"], 1)
def test_test_smtp_connection_uses_starttls_and_login(self) -> None:
settings = SimpleNamespace(
smtp_host="smtp.example.com",
smtp_port=587,
smtp_username="mailer@example.com",
smtp_password="secret",
smtp_from_address="alias@example.com",
smtp_from_name="Media Library Viewer",
smtp_use_tls=True,
smtp_use_ssl=False,
smtp_timeout=15,
)
smtp = MagicMock()
smtp_factory = MagicMock(return_value=_SMTPContext(smtp))
with patch("media_library_viewer_api.services.mailer.smtplib.SMTP", smtp_factory), patch(
"media_library_viewer_api.services.mailer.smtplib.SMTP_SSL"
) as smtp_ssl:
result = smtp_connection_probe(settings)
smtp_ssl.assert_not_called()
smtp.ehlo.assert_called()
smtp.starttls.assert_called_once()
smtp.login.assert_called_once_with("mailer@example.com", "secret")
smtp.noop.assert_called_once()
self.assertEqual(result["status"], "ok")
self.assertEqual(result["from_address"], "alias@example.com")
self.assertEqual(result["smtp_host"], "smtp.example.com")
self.assertEqual(result["selected_mode"]["label"], "configured")
def test_test_smtp_connection_falls_back_to_fastmail_mode(self) -> None:
settings = SimpleNamespace(
smtp_host="smtp.fastmail.com",
smtp_port=465,
smtp_username="mailer@example.com",
smtp_password="secret",
smtp_from_address="alias@example.com",
smtp_from_name="Media Library Viewer",
smtp_use_tls=False,
smtp_use_ssl=True,
smtp_timeout=15,
)
smtp_ssl = MagicMock(side_effect=TimeoutError("timed out"))
fallback_smtp = MagicMock()
smtp_factory = MagicMock(return_value=_SMTPContext(fallback_smtp))
with patch("media_library_viewer_api.services.mailer.smtplib.SMTP_SSL", smtp_ssl), patch(
"media_library_viewer_api.services.mailer.smtplib.SMTP",
smtp_factory,
):
result = smtp_connection_probe(settings)
self.assertEqual(result["status"], "ok")
self.assertEqual(result["selected_mode"]["label"], "Fastmail STARTTLS 587")
self.assertEqual(len(result["attempts"]), 2)
self.assertEqual(result["attempts"][0]["status"], "failed")
self.assertEqual(result["attempts"][1]["status"], "ok")
fallback_smtp.starttls.assert_called_once()
fallback_smtp.login.assert_called_once_with("mailer@example.com", "secret")
def test_send_email_message_falls_back_to_fastmail_mode(self) -> None:
settings = SimpleNamespace(
smtp_host="smtp.fastmail.com",
@@ -138,7 +76,7 @@ class MailerTests(unittest.TestCase):
smtp_username="mailer@example.com",
smtp_password="secret",
smtp_from_address="alias@example.com",
smtp_from_name="Media Library Viewer",
smtp_from_name="Manage",
smtp_use_tls=False,
smtp_use_ssl=True,
smtp_timeout=15,
@@ -172,7 +110,7 @@ class MailerTests(unittest.TestCase):
smtp_username="mailer@example.com",
smtp_password="secret",
smtp_from_address="alias@example.com",
smtp_from_name="Media Library Viewer",
smtp_from_name="Manage",
smtp_use_tls=True,
smtp_use_ssl=False,
smtp_timeout=15,