From 4cdb4b144ccb8b71eed0f7e677e47887e6131ad4 Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Mon, 4 May 2026 16:59:06 +0200 Subject: [PATCH] simplifications and refactorings --- .env.example | 2 +- README.md | 4 +- backend/README.md | 4 +- backend/pyproject.toml | 2 +- .../src/media_library_viewer_api/config.py | 2 +- backend/src/media_library_viewer_api/main.py | 4 +- .../media_library_viewer_api/routers/users.py | 9 +- .../services/mailer.py | 130 +----------------- backend/tests/test_api.py | 109 +-------------- backend/tests/test_mailer.py | 70 +--------- docs/REQUIREMENTS.md | 4 +- frontend/README.md | 4 +- frontend/index.html | 2 +- frontend/src/App.tsx | 4 +- frontend/src/api/client.ts | 4 - frontend/src/hooks/useTestUserSmtp.ts | 8 -- frontend/src/pages/Users.tsx | 92 +------------ frontend/src/types/index.ts | 32 ----- 18 files changed, 30 insertions(+), 456 deletions(-) delete mode 100644 frontend/src/hooks/useTestUserSmtp.ts diff --git a/.env.example b/.env.example index 37000f8..269b4c4 100644 --- a/.env.example +++ b/.env.example @@ -16,7 +16,7 @@ SMTP_PORT=587 SMTP_USERNAME=your-smtp-username SMTP_PASSWORD=your-smtp-password SMTP_FROM_ADDRESS=no-reply@example.com -SMTP_FROM_NAME=Media Library Viewer +SMTP_FROM_NAME=Manage SMTP_USE_TLS=true SMTP_USE_SSL=false SMTP_TIMEOUT=30 diff --git a/README.md b/README.md index 3ea3b45..c5a9bac 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Media Library Viewer +# Manage -Media library management tool with Jellyfin integration, SSH file inspection, server monitoring, and safe remote job templates. +Manage is a media and server operations tool with Jellyfin integration, SSH file inspection, server monitoring, and safe remote job templates. See `docs/REQUIREMENTS.md` for the living requirements, decisions, and planning history. See `docs/MIGRATION_PLAN.md` for the FastAPI + React architecture plan. diff --git a/backend/README.md b/backend/README.md index 99c32fe..928b6b6 100644 --- a/backend/README.md +++ b/backend/README.md @@ -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 diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 627145a..c0f673b 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -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", diff --git a/backend/src/media_library_viewer_api/config.py b/backend/src/media_library_viewer_api/config.py index 3f9dca6..14c5957 100644 --- a/backend/src/media_library_viewer_api/config.py +++ b/backend/src/media_library_viewer_api/config.py @@ -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 diff --git a/backend/src/media_library_viewer_api/main.py b/backend/src/media_library_viewer_api/main.py index 376015e..098d686 100644 --- a/backend/src/media_library_viewer_api/main.py +++ b/backend/src/media_library_viewer_api/main.py @@ -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, ) diff --git a/backend/src/media_library_viewer_api/routers/users.py b/backend/src/media_library_viewer_api/routers/users.py index bf57e11..6de1f9f 100644 --- a/backend/src/media_library_viewer_api/routers/users.py +++ b/backend/src/media_library_viewer_api/routers/users.py @@ -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(...), diff --git a/backend/src/media_library_viewer_api/services/mailer.py b/backend/src/media_library_viewer_api/services/mailer.py index e1e4eae..c840e29 100644 --- a/backend/src/media_library_viewer_api/services/mailer.py +++ b/backend/src/media_library_viewer_api/services/mailer.py @@ -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 "", - ) - - 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)) diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index d2767d4..6057eb9 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -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, diff --git a/backend/tests/test_mailer.py b/backend/tests/test_mailer.py index 3c231fa..63e5ab5 100644 --- a/backend/tests/test_mailer.py +++ b/backend/tests/test_mailer.py @@ -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 ") + self.assertEqual(message["From"], "Manage ") 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, diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index a0f2abc..948cf6c 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -1,10 +1,10 @@ -# Media Library Viewer - Requirements and Decision Log +# Manage - Requirements and Decision Log This is a living document for the project. Update it whenever requirements, UX expectations, architecture decisions, constraints, or implementation plans change. ## Product Goal -Build a compact Streamlit application for browsing a remote Jellyfin media library and inspecting the corresponding media files on disk over SSH. The app should support library metadata review, direct file-system navigation, detailed media metadata inspection, and safe remote maintenance/job workflows. +Build Manage, a compact web application for browsing a remote Jellyfin media library and inspecting the corresponding media files on disk over SSH. The app should support library metadata review, direct file-system navigation, detailed media metadata inspection, and safe remote maintenance/job workflows. ## Current Phase diff --git a/frontend/README.md b/frontend/README.md index ade82bb..f1d733b 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,6 +1,6 @@ -# Frontend - Media Library Viewer +# Manage Frontend -React + TypeScript SPA for the Media Library Viewer, consuming the FastAPI backend. +React + TypeScript SPA for Manage, consuming the FastAPI backend. ## Tech Stack diff --git a/frontend/index.html b/frontend/index.html index 0fca6f0..a4c0b83 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - frontend + Manage
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 61bec96..86f100f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -56,7 +56,7 @@ function Shell({ - Media Library Viewer + Manage void }) { Sign in required - Use your Authentik account to access the media library viewer. + Use your Authentik account to access Manage. -