Skip to content

Commit 40f7c3a

Browse files
committed
Keep unrelated Plex changes out of #949 fix
1 parent 88eacfa commit 40f7c3a

3 files changed

Lines changed: 97 additions & 21 deletions

File tree

docs/agents/plex_integration.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ The system prioritizes **TMDB IDs** as the canonical source of truth. All Plex e
1111
1. **Explicit TMDB ID**: Extracted directly from Plex GUIDs (e.g., `tmdb://123`).
1212
2. **IMDB/TVDB Lookup**: If only IMDB (`tt123`) or TVDB (`789`) IDs are present, the system queries the TMDB `find` API to resolve the corresponding `tmdb_id`.
1313
3. **Title Search Fallback**:
14-
- If no external IDs are found (or they return 404s), the system performs a search against TMDB using the media title.
15-
- **TV Shows**: Uses `grandparentTitle` (series title) or `title`. Match attempts to filter by year if available.
14+
- For TV history entries, Plex item metadata and then show metadata from `grandparentRatingKey`/`grandparentKey` are checked before title search.
15+
- If no deterministic IDs are found (or they return 404s), the system performs a search against TMDB using the media title.
16+
- **TV Shows**: Uses `grandparentTitle` (series title) or `title` only after the Plex show-level lookup. Match attempts to filter by year if available.
1617
- **Movies**: Uses `title` and `year`.
1718

1819
**GUID extraction & conflicts:**
@@ -42,7 +43,7 @@ Plex history import uses Plex's history endpoint as the canonical event stream.
4243
- Sorted newest-first (`sort=viewedAt:desc`) and paged with `X-Plex-Container-Start`/`X-Plex-Container-Size`. `PLEX_HISTORY_PAGE_SIZE` controls page size; `PLEX_HISTORY_MAX_ITEMS` (0 = no cap) limits how far back we fetch. There is no time windowing, so re-importing overlapping ranges is expected.
4344

4445
**Fields we rely on:**
45-
- IDs: `Guid`/`guid` entries with TMDB/IMDB/TVDB identifiers are required for deterministic resolution. Order is: resolve IDs from the history row (including title search when allowed); if missing, fetch `GET {server_uri}/library/metadata/{ratingKey}` to pull GUIDs (no title search in this step); if still missing, the movie/TV recorders may still fall back to title search when a title is available, otherwise the entry is skipped.
46+
- IDs: `Guid`/`guid` entries with TMDB/IMDB/TVDB identifiers are required for deterministic resolution. For TV history, resolve IDs from the history row, fetch `GET {server_uri}/library/metadata/{ratingKey}` when needed, then inspect the Plex show at `grandparentRatingKey`/`grandparentKey` before allowing title search. Movies retain their existing title fallback when a title is available; otherwise the entry is skipped.
4647
- Titles: `title` or `grandparentTitle` is required for title-search fallback; Plex-only GUIDs without a title are skipped.
4748
- Timing: `viewedAt` or `lastViewedAt` (epoch seconds) is the authoritative `watched_at`. If missing, we fall back to import time; `viewCount`/`viewOffset` are ignored. Rows missing `viewedAt`/`lastViewedAt` are nondeterministic and can dedupe poorly across runs.
4849
- TV structure: `parentIndex` (season) and `index` (episode) must be numeric. Missing numbers can cause the entry to be skipped or treated as a movie in show libraries.

src/integrations/imports/plex.py

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,6 @@ def __init__(self, user, account, mode, library, fast_mode=True):
130130

131131
def import_data(self):
132132
"""Import history for the selected library."""
133-
self._ensure_username_matches()
134133
self._ensure_account_id()
135134
self._init_allowed_usernames()
136135
self._init_allowed_account_ids()
@@ -232,23 +231,6 @@ def import_data(self):
232231
deduped_warnings = "\n".join(dict.fromkeys(self.warnings))
233232
return result_counts, deduped_warnings
234233

235-
def _ensure_username_matches(self):
236-
"""Persist the Plex username into the user's webhook allow list."""
237-
username = (self.account.plex_username or "").strip()
238-
if not username:
239-
return
240-
241-
existing = [
242-
u.strip() for u in (self.user.plex_usernames or "").split(",") if u.strip()
243-
]
244-
245-
if username.lower() in [u.lower() for u in existing]:
246-
return
247-
248-
updated = [*existing, username]
249-
self.user.plex_usernames = ", ".join(updated)
250-
self.user.save(update_fields=["plex_usernames"])
251-
252234
def _ensure_account_id(self):
253235
"""Fetch and persist the Plex account id if missing."""
254236
if self._account_id:

src/integrations/tests/test_plex_import_logic.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1456,6 +1456,99 @@ def test_import_that_creates_nothing_does_not_reload_the_calendar(
14561456
mock_schedule_stats.assert_called_once_with(self.user.id)
14571457

14581458

1459+
class TestPlexUsernameImportBehavior(TestCase):
1460+
"""Plex imports must not rewrite the user's configured username filters."""
1461+
1462+
def setUp(self):
1463+
User = get_user_model()
1464+
self.user = User.objects.create_user(username="plex-import-user")
1465+
self.account = PlexAccount.objects.create(
1466+
user=self.user,
1467+
plex_token="token",
1468+
plex_username="server-user",
1469+
plex_account_id="9999",
1470+
)
1471+
self.user.plex_usernames = "user1, user2"
1472+
self.user.save(update_fields=["plex_usernames"])
1473+
1474+
def test_successful_import_preserves_explicit_username_list(self):
1475+
"""A completed import must not add the connected server account."""
1476+
with (
1477+
patch("integrations.imports.plex.plex_api.list_users", return_value=[]),
1478+
patch(
1479+
"integrations.imports.plex.plex_api.list_sections",
1480+
return_value=[
1481+
{
1482+
"id": "1",
1483+
"machine_identifier": "machine",
1484+
"title": "Movies",
1485+
"type": "movie",
1486+
"uri": "http://plex",
1487+
}
1488+
],
1489+
),
1490+
patch(
1491+
"integrations.imports.plex.plex_api.list_resources",
1492+
return_value=[
1493+
{
1494+
"machine_identifier": "machine",
1495+
"connections": [{"uri": "http://plex"}],
1496+
}
1497+
],
1498+
),
1499+
patch(
1500+
"integrations.imports.plex.plex_api.fetch_history",
1501+
return_value=([], 0),
1502+
),
1503+
patch(
1504+
"integrations.imports.plex.plex_api.fetch_section_all_items",
1505+
return_value=([], 0),
1506+
),
1507+
):
1508+
plex.importer("all", self.user, "new")
1509+
1510+
self.user.refresh_from_db()
1511+
self.assertEqual(self.user.plex_usernames, "user1, user2")
1512+
1513+
def test_failed_import_preserves_explicit_username_list(self):
1514+
"""An early Plex failure must not add the connected server account."""
1515+
from integrations.plex import PlexAuthError
1516+
1517+
with (
1518+
patch("integrations.imports.plex.plex_api.list_users", return_value=[]),
1519+
patch(
1520+
"integrations.imports.plex.plex_api.list_resources",
1521+
side_effect=PlexAuthError("token expired"),
1522+
),
1523+
):
1524+
with self.assertRaises(helpers.MediaImportError):
1525+
plex.importer("all", self.user, "new")
1526+
1527+
self.user.refresh_from_db()
1528+
self.assertEqual(self.user.plex_usernames, "user1, user2")
1529+
1530+
def test_empty_username_list_uses_connected_account_without_persisting(self):
1531+
"""An empty list still filters to the connected account without saving it."""
1532+
self.user.plex_usernames = ""
1533+
self.user.save(update_fields=["plex_usernames"])
1534+
importer = PlexHistoryImporter(
1535+
user=self.user,
1536+
account=self.account,
1537+
mode="new",
1538+
library="all",
1539+
)
1540+
1541+
with patch("integrations.imports.plex.plex_api.list_users", return_value=[]):
1542+
importer._init_allowed_usernames()
1543+
importer._init_allowed_account_ids()
1544+
1545+
self.assertEqual(importer._allowed_usernames, ["server-user"])
1546+
self.assertEqual(importer._allowed_account_ids, {"9999"})
1547+
self.assertTrue(importer._is_allowed_history_user({"accountID": "9999"}))
1548+
self.user.refresh_from_db()
1549+
self.assertEqual(self.user.plex_usernames, "")
1550+
1551+
14591552
class TestPlexMultiServerImport(TestCase):
14601553
"""Tests for multi-server / shared-library import resilience."""
14611554

0 commit comments

Comments
 (0)