From 2803777ebef872fe0c1166c6ee65c95b87900604 Mon Sep 17 00:00:00 2001 From: Sam Oehlert Date: Mon, 20 Jan 2025 12:49:08 -0600 Subject: [PATCH 1/8] feat(myplexaccount): use plex authentication instead of raw server token for connection this should allow remote server choices, easy switching, and less hardcoded stuff --- backend/config.py | 5 +++-- backend/services/plex.py | 32 ++++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/backend/config.py b/backend/config.py index e1e776e..ad7e5e2 100644 --- a/backend/config.py +++ b/backend/config.py @@ -6,8 +6,9 @@ class Settings(BaseSettings): """Define the settings we need.""" - plex_base_url: str - plex_token: str + plex_username: str + plex_password: str + plex_server_name: str client_name: str redis_url: str tunebox_url: str diff --git a/backend/services/plex.py b/backend/services/plex.py index dd06e39..661431d 100644 --- a/backend/services/plex.py +++ b/backend/services/plex.py @@ -6,8 +6,9 @@ import requests import urllib3 from fastapi import HTTPException +from functools import lru_cache from plexapi.exceptions import PlexApiException -from plexapi.server import PlexServer +from plexapi.myplex import MyPlexAccount from backend.config import settings from backend.services.redis import cache_data, get_cached_data, get_redis_queue, remove_from_redis_queue @@ -23,15 +24,31 @@ playback_active = False +@lru_cache() def get_plex_connection(): - """Establish a connection to the Plex server. + """Establish a connection to the Plex server via MyPlexAccount. Returns: A PlexServer instance to run our API calls against. """ - session = requests.Session() - session.verify = False - return PlexServer(settings.plex_base_url, settings.plex_token, session=session) + try: + session = requests.Session() + session.verify = False + + # Connect to Plex via MyPlexAccount + account = MyPlexAccount( + username=settings.plex_username, + password=settings.plex_password + ) + + # Get the specific server by its name + plex_server = account.resource(settings.plex_server_name).connect() + logger.info("Connected to Plex server %s", plex_server) + + return plex_server + + except Exception as e: + raise Exception(f"Failed to connect to Plex server: {str(e)}") def calculate_playback_state(session): @@ -401,7 +418,10 @@ def fetch_art(item_id: int, item_type: str): if not item.thumb: raise HTTPException(status_code=404, detail=f"No image available for this {item_type}.") - image_url = f"{settings.plex_base_url}{item.thumb}?X-Plex-Token={settings.plex_token}" + # Get the server URL and token from the established connection + server_url = plex._baseurl + token = plex._token + image_url = f"{server_url}{item.thumb}?X-Plex-Token={token}" # ruff: noqa: S501 response = requests.get(image_url, stream=True, verify=False, timeout=5) From c178b68b2a2c5afc44167caaf00b79c4485a5432 Mon Sep 17 00:00:00 2001 From: Sam Oehlert Date: Mon, 20 Jan 2025 12:50:34 -0600 Subject: [PATCH 2/8] docs(plex-auth): update docs to show new auth method --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a176daa..d29cd43 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,11 @@ TuneBox is built with the following technologies: 3. Create a .env file in the root directory and configure it with your Plex server details. The .env file should look like this (where CLIENT_NAME is according to your Plex client): ```bash - PLEX_BASE_URL=your_plex_server_ip # Example: https:/192.0.2.0:32400 - PLEX_TOKEN=your_plex_token + PLEX_USERNAME=usernamehere + PLEX_PASSWORD=passwordhere + PLEX_SERVER_NAME=plex-server CLIENT_NAME=Macbook Pro Personal - REDIS_URL=redis://localhost:6379 + REDIS_URL=redis://redis:6379 TUNEBOX_URL=localhost:8000 # or DNS name or IP address of your Tunebox host 4. Create a frontend/.env file and configure it with. Unfortunately Vite requires us to use a separate .env file inside the frontend directory From eee839f53c0ee5cfecfc58c6d3fe32a132222284 Mon Sep 17 00:00:00 2001 From: Sam Oehlert Date: Mon, 20 Jan 2025 12:59:27 -0600 Subject: [PATCH 3/8] fix(nginx): fix nginx config to not duplicate ports and types --- Compose/frontend/nginx.conf | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Compose/frontend/nginx.conf b/Compose/frontend/nginx.conf index 0298233..84a3d85 100644 --- a/Compose/frontend/nginx.conf +++ b/Compose/frontend/nginx.conf @@ -2,14 +2,6 @@ events {} http { include mime.types; - types { - text/html html; - text/css css; - application/javascript js mjs; - image/png png; - image/jpeg jpeg jpg; - application/json json; - } server { listen 80; @@ -20,7 +12,7 @@ http { # Reverse proxy for the API location /api/ { - proxy_pass http://${TUNEBOX_URL}:8000; + proxy_pass http://${TUNEBOX_URL}; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -29,7 +21,7 @@ http { # Reverse proxy for WebSocket connections location /ws { - proxy_pass http://${TUNEBOX_URL}:8000; + proxy_pass http://${TUNEBOX_URL}; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; From 4d8e5a3c58e4c3c2c0ef06b21423599c8b96edd4 Mon Sep 17 00:00:00 2001 From: Sam Oehlert Date: Mon, 20 Jan 2025 12:59:55 -0600 Subject: [PATCH 4/8] fix(envvars): make sure the right envvars are available --- docker-compose.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 2cbcc7c..6681082 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,10 @@ services: dockerfile: Compose/frontend/Dockerfile ports: - "80:80" + environment: + - TUNEBOX_URL=${TUNEBOX_URL} + volumes: + - ./frontend/.env:/app/.env depends_on: - backend From 9582f68769974662f600bd60754ff7135b97ae64 Mon Sep 17 00:00:00 2001 From: Sam Oehlert Date: Mon, 20 Jan 2025 13:16:04 -0600 Subject: [PATCH 5/8] test(plex-auth): update backend tests to match the new config file --- backend/tests/conftest.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 0514e31..571df5c 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -6,8 +6,9 @@ from plexapi.audio import Track mock_env = { - "PLEX_BASE_URL": "http://fake-plex:32400", - "PLEX_TOKEN": "fake-token", + "PLEX_USERNAME": "testuser", + "PLEX_PASSWORD": "testpassword", + "PLEX_SERVER_NAME": "plexserver", "CLIENT_NAME": "test-client", "REDIS_URL": "redis://fake-redis:6379", "TUNEBOX_URL": "http://fake-tunebox:8000", From a1e62513572e99c6612ae394e3c17a9168a9858e Mon Sep 17 00:00:00 2001 From: Sam Oehlert Date: Mon, 20 Jan 2025 13:26:28 -0600 Subject: [PATCH 6/8] refactor(ruff): make changes to have our own exceptions and make sure ruff formatting is happy --- backend/exceptions.py | 15 +++++++++++++++ backend/services/plex.py | 16 ++++++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 backend/exceptions.py diff --git a/backend/exceptions.py b/backend/exceptions.py new file mode 100644 index 0000000..64e1170 --- /dev/null +++ b/backend/exceptions.py @@ -0,0 +1,15 @@ +"""Custom exceptions for the backend application.""" +from typing import Optional + + +class PlexConnectionError(Exception): + """Exception raised for Plex connection errors. + + Attributes: + original_error (Optional[Exception]): The original exception that caused this error + """ + default_message = "Failed to connect to Plex server" + + def __init__(self, original_error: Optional[Exception] = None) -> None: + self.original_error = original_error + super().__init__(self.default_message) \ No newline at end of file diff --git a/backend/services/plex.py b/backend/services/plex.py index 661431d..ef1637b 100644 --- a/backend/services/plex.py +++ b/backend/services/plex.py @@ -2,15 +2,16 @@ import asyncio import logging +from functools import lru_cache import requests import urllib3 from fastapi import HTTPException -from functools import lru_cache from plexapi.exceptions import PlexApiException from plexapi.myplex import MyPlexAccount from backend.config import settings +from backend.exceptions import PlexConnectionError from backend.services.redis import cache_data, get_cached_data, get_redis_queue, remove_from_redis_queue from backend.utils import TrackTimeTracker, milliseconds_to_seconds @@ -24,7 +25,7 @@ playback_active = False -@lru_cache() +@lru_cache def get_plex_connection(): """Establish a connection to the Plex server via MyPlexAccount. @@ -44,11 +45,10 @@ def get_plex_connection(): # Get the specific server by its name plex_server = account.resource(settings.plex_server_name).connect() logger.info("Connected to Plex server %s", plex_server) - - return plex_server - except Exception as e: - raise Exception(f"Failed to connect to Plex server: {str(e)}") + raise PlexConnectionError(original_error=e) from e + else: + return plex_server def calculate_playback_state(session): @@ -419,8 +419,8 @@ def fetch_art(item_id: int, item_type: str): raise HTTPException(status_code=404, detail=f"No image available for this {item_type}.") # Get the server URL and token from the established connection - server_url = plex._baseurl - token = plex._token + server_url = plex.baseurl + token = plex.token image_url = f"{server_url}{item.thumb}?X-Plex-Token={token}" # ruff: noqa: S501 From 5d4d415fe1f26711730457b280225a0abdf7f0d6 Mon Sep 17 00:00:00 2001 From: Sam Oehlert Date: Mon, 20 Jan 2025 13:28:49 -0600 Subject: [PATCH 7/8] style(ruff): make changes for ruff here as well --- backend/exceptions.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/exceptions.py b/backend/exceptions.py index 64e1170..ff41e12 100644 --- a/backend/exceptions.py +++ b/backend/exceptions.py @@ -1,5 +1,4 @@ """Custom exceptions for the backend application.""" -from typing import Optional class PlexConnectionError(Exception): @@ -10,6 +9,12 @@ class PlexConnectionError(Exception): """ default_message = "Failed to connect to Plex server" - def __init__(self, original_error: Optional[Exception] = None) -> None: + def __init__(self, original_error: Exception | None = None) -> None: + """Initialize the PlexConnectionError. + + Args: + original_error (Optional[Exception], optional): The original exception that caused this error. + Defaults to None. + """ self.original_error = original_error super().__init__(self.default_message) \ No newline at end of file From a4b11963f5be604f890b9700de9e07b82f2fb4a6 Mon Sep 17 00:00:00 2001 From: Sam Oehlert Date: Mon, 20 Jan 2025 13:31:12 -0600 Subject: [PATCH 8/8] style(ruff): ruff format as well --- backend/exceptions.py | 3 ++- backend/services/plex.py | 5 +---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/backend/exceptions.py b/backend/exceptions.py index ff41e12..cca6333 100644 --- a/backend/exceptions.py +++ b/backend/exceptions.py @@ -7,6 +7,7 @@ class PlexConnectionError(Exception): Attributes: original_error (Optional[Exception]): The original exception that caused this error """ + default_message = "Failed to connect to Plex server" def __init__(self, original_error: Exception | None = None) -> None: @@ -17,4 +18,4 @@ def __init__(self, original_error: Exception | None = None) -> None: Defaults to None. """ self.original_error = original_error - super().__init__(self.default_message) \ No newline at end of file + super().__init__(self.default_message) diff --git a/backend/services/plex.py b/backend/services/plex.py index ef1637b..bfa612a 100644 --- a/backend/services/plex.py +++ b/backend/services/plex.py @@ -37,10 +37,7 @@ def get_plex_connection(): session.verify = False # Connect to Plex via MyPlexAccount - account = MyPlexAccount( - username=settings.plex_username, - password=settings.plex_password - ) + account = MyPlexAccount(username=settings.plex_username, password=settings.plex_password) # Get the specific server by its name plex_server = account.resource(settings.plex_server_name).connect()