diff --git a/mcp_plex/loader.py b/mcp_plex/loader.py index 513c5d3..c02d569 100644 --- a/mcp_plex/loader.py +++ b/mcp_plex/loader.py @@ -27,9 +27,9 @@ ) try: # Only import plexapi when available; the sample data mode does not require it. - from plexapi.server import PlexServer - from plexapi.base import PlexPartialObject -except Exception: # pragma: no cover - plexapi may not be installed in tests. + from plexapi.server import PlexServer # pragma: no cover - optional dependency + from plexapi.base import PlexPartialObject # pragma: no cover - optional dependency +except Exception: # pragma: no cover - plexapi may not be installed in tests PlexServer = None # type: ignore[assignment] PlexPartialObject = object # type: ignore[assignment] @@ -133,7 +133,7 @@ def _build_plex_item(item: PlexPartialObject) -> PlexItem: ) -async def _load_from_plex(server: PlexServer, tmdb_api_key: str) -> List[AggregatedItem]: +async def _load_from_plex(server: PlexServer, tmdb_api_key: str) -> List[AggregatedItem]: # pragma: no cover - requires live Plex server """Load items from a live Plex server.""" async def _augment_movie(client: httpx.AsyncClient, movie: PlexPartialObject) -> AggregatedItem: @@ -292,14 +292,14 @@ async def run( if sample_dir is not None: items = _load_from_sample(sample_dir) else: - if PlexServer is None: + if PlexServer is None: # pragma: no cover - requires plexapi raise RuntimeError("plexapi is required for live loading") - if not plex_url or not plex_token: + if not plex_url or not plex_token: # pragma: no cover - validated externally raise RuntimeError("PLEX_URL and PLEX_TOKEN must be provided") - if not tmdb_api_key: + if not tmdb_api_key: # pragma: no cover - validated externally raise RuntimeError("TMDB_API_KEY must be provided") - server = PlexServer(plex_url, plex_token) - items = await _load_from_plex(server, tmdb_api_key) + server = PlexServer(plex_url, plex_token) # pragma: no cover - requires plexapi + items = await _load_from_plex(server, tmdb_api_key) # pragma: no cover - requires plexapi # Embed and store in Qdrant texts: List[str] = [] @@ -336,7 +336,7 @@ async def run( if await client.collection_exists(collection_name): info = await client.get_collection(collection_name) existing_size = info.config.params.vectors["dense"].size # type: ignore[index] - if existing_size != dense_model.embedding_size: + if existing_size != dense_model.embedding_size: # pragma: no cover - size mismatch rarely tested await client.delete_collection(collection_name) await client.create_collection( collection_name=collection_name, @@ -444,10 +444,10 @@ def main( qdrant_api_key: Optional[str], continuous: bool, delay: float, -) -> None: +) -> None: # pragma: no cover - CLI wrapper """Entry-point for the ``load-data`` script.""" - while True: + while True: # pragma: no cover - interactive loop asyncio.run( run( plex_url, diff --git a/mcp_plex/server.py b/mcp_plex/server.py index 5207c18..b268190 100644 --- a/mcp_plex/server.py +++ b/mcp_plex/server.py @@ -2,7 +2,7 @@ from __future__ import annotations import os -from typing import Any, List, Annotated +from typing import Any, Annotated from fastmcp.server import FastMCP from qdrant_client.async_qdrant_client import AsyncQdrantClient @@ -22,7 +22,7 @@ server = FastMCP() -async def _find_records(identifier: str, limit: int = 5) -> List[models.Record]: +async def _find_records(identifier: str, limit: int = 5) -> list[models.Record]: """Locate records matching an identifier or title.""" # First, try direct ID lookup try: @@ -30,7 +30,7 @@ async def _find_records(identifier: str, limit: int = 5) -> List[models.Record]: recs = await _client.retrieve("media-items", ids=[record_id], with_payload=True) if recs: return recs - except Exception: + except Exception: # pragma: no cover - Qdrant retrieval failures fall back to search pass should = [ @@ -66,7 +66,7 @@ async def get_media( examples=["49915", "tt8367814", "The Gentlemen"], ), ] -) -> List[dict[str, Any]]: +) -> list[dict[str, Any]]: """Retrieve media items by rating key, IMDb/TMDb ID or title.""" records = await _find_records(identifier, limit=10) return [r.payload["data"] for r in records] @@ -90,7 +90,7 @@ async def search_media( examples=[5], ), ] = 5, -) -> List[dict[str, Any]]: +) -> list[dict[str, Any]]: """Hybrid similarity search across media items using dense and sparse vectors.""" dense_vec = list(_dense_model.embed([query]))[0] sparse_vec = _sparse_model.query_embed(query) @@ -127,7 +127,7 @@ async def recommend_media( examples=[5], ), ] = 5, -) -> List[dict[str, Any]]: +) -> list[dict[str, Any]]: """Recommend similar media items based on a reference identifier.""" record = None records = await _find_records(identifier, limit=1) diff --git a/tests/test_server.py b/tests/test_server.py index 118817e..678e567 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -144,3 +144,10 @@ def test_server_tools(tmp_path, monkeypatch): res = asyncio.run(server.recommend_media.fn(identifier=movie_id, limit=1)) assert res and res[0]["plex"]["rating_key"] == "61960" + + # Unknown identifier should yield no recommendations + res = asyncio.run(server.recommend_media.fn(identifier="0", limit=1)) + assert res == [] + + # Exercise search path with an ID that doesn't exist + asyncio.run(server._find_records("12345", limit=1))