Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
avoid unnecessary model downloads when reranking is disabled or unavailable.
- Media payload and artwork caching centralized in `MediaCache` attached to
`PlexServer` to streamline cache management and testing.
- Plex episodes with year-based seasons are mapped to the correct TMDb season
numbers via a helper that matches season names or air-date years to ensure
accurate episode lookups.

## User Queries
The project should handle natural-language searches and recommendations such as:
Expand Down
56 changes: 51 additions & 5 deletions mcp_plex/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,56 @@ async def _fetch_tmdb_episode(
return None


def resolve_tmdb_season_number(
show_tmdb: Optional[TMDBShow], episode: PlexPartialObject
) -> Optional[int]:
"""Map a Plex episode to the appropriate TMDb season number.

This resolves cases where Plex uses year-based season indices that do not
match TMDb's sequential ``season_number`` values.
"""

parent_index = getattr(episode, "parentIndex", None)
parent_title = getattr(episode, "parentTitle", None)

seasons = getattr(show_tmdb, "seasons", []) if show_tmdb else []

# direct numeric match
if parent_index is not None:
for season in seasons:
if season.season_number == parent_index:
return season.season_number

# match by season name (e.g. "Season 2018" -> "2018")
title_norm: Optional[str] = None
if isinstance(parent_title, str):
title_norm = parent_title.lower().lstrip("season ").strip()
for season in seasons:
name_norm = (season.name or "").lower().lstrip("season ").strip()
if name_norm == title_norm:
return season.season_number

# match by air date year when Plex uses year-based seasons
year: Optional[int] = None
if isinstance(parent_index, int):
year = parent_index
elif title_norm and title_norm.isdigit():
year = int(title_norm)

if year is not None:
for season in seasons:
air = getattr(season, "air_date", None)
if isinstance(air, str) and len(air) >= 4 and air[:4].isdigit():
if int(air[:4]) == year:
return season.season_number

if parent_index is not None:
return int(parent_index)
if isinstance(parent_title, str) and parent_title.isdigit():
return int(parent_title)
return None


def _extract_external_ids(item: PlexPartialObject) -> ExternalIDs:
"""Extract IMDb and TMDb IDs from a Plex object."""

Expand Down Expand Up @@ -245,11 +295,7 @@ async def _augment_episode(
imdb_task = (
_fetch_imdb(client, ids.imdb) if ids.imdb else asyncio.sleep(0, result=None)
)
season = getattr(episode, "parentIndex", None)
if season is None:
title = getattr(episode, "parentTitle", "")
if isinstance(title, str) and title.isdigit():
season = int(title)
season = resolve_tmdb_season_number(show_tmdb, episode)
ep_num = getattr(episode, "index", None)
tmdb_task = (
_fetch_tmdb_episode(client, show_tmdb.id, season, ep_num, tmdb_api_key)
Expand Down
8 changes: 8 additions & 0 deletions mcp_plex/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ class TMDBGenre(BaseModel):
name: str


class TMDBSeason(BaseModel):
season_number: int
name: str
air_date: Optional[str] = None


class TMDBMovie(BaseModel):
id: int
title: str
Expand Down Expand Up @@ -77,6 +83,7 @@ class TMDBShow(BaseModel):
first_air_date: Optional[str] = None
last_air_date: Optional[str] = None
genres: List[TMDBGenre] = Field(default_factory=list)
seasons: List[TMDBSeason] = Field(default_factory=list)
poster_path: Optional[str] = None
backdrop_path: Optional[str] = None
tagline: Optional[str] = None
Expand Down Expand Up @@ -153,6 +160,7 @@ class ExternalIDs:
"IMDbName",
"TMDBGenre",
"TMDBMovie",
"TMDBSeason",
"TMDBShow",
"TMDBEpisode",
"TMDBItem",
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "mcp-plex"
version = "0.26.15"
version = "0.26.17"

description = "Plex-Oriented Model Context Protocol Server"
requires-python = ">=3.11,<3.13"
Expand Down
22 changes: 22 additions & 0 deletions tests/test_loader_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
_load_imdb_retry_queue,
_persist_imdb_retry_queue,
_process_imdb_retry_queue,
resolve_tmdb_season_number,
)
from mcp_plex.types import TMDBSeason, TMDBShow


def test_extract_external_ids():
Expand Down Expand Up @@ -240,3 +242,23 @@ async def second_run():
asyncio.run(second_run())
assert json.loads(queue_path.read_text()) == []
assert loader._imdb_cache.get("tt1") is not None


def test_resolve_tmdb_season_number_matches_name():
episode = types.SimpleNamespace(parentIndex=2018, parentTitle="2018")
show = TMDBShow(
id=1,
name="Show",
seasons=[TMDBSeason(season_number=14, name="2018")],
)
assert resolve_tmdb_season_number(show, episode) == 14


def test_resolve_tmdb_season_number_matches_air_date():
episode = types.SimpleNamespace(parentIndex=2018, parentTitle="Season 2018")
show = TMDBShow(
id=1,
name="Show",
seasons=[TMDBSeason(season_number=16, name="Season 16", air_date="2018-01-03")],
)
assert resolve_tmdb_season_number(show, episode) == 16
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.