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
4 changes: 2 additions & 2 deletions Emby-DLL/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Chronarr Emby Plugin — Changelog

## v2.0.21 — Library Exclusions Fix
- Fixed library exclusions not actually excluding anything — the config page saved a library's ID in a different GUID format than the real-time sync and scheduled task compared it against, so a checked-off library still got scanned
## v2.0.22 — Library Exclusions Fix
- Fixed library exclusions not actually excluding anything — Emby's virtual-folder API exposes three different ID fields for a library (`Guid`, `ItemId`, `Id`), and the config page was capturing the wrong one (`Id`, a short legacy numeric string on some Emby versions, not a GUID at all). That never matched what the real-time sync and scheduled task compare against, so a checked-off library still got scanned regardless of which library it was. Now reads the library's actual `Guid` field — **re-check and re-save Library Exclusions after updating** so the corrected IDs get stored.

## v2.0.20 — Server Name & Admin Pre-fill
- Server name field is now read-only and sourced automatically from Emby's configured server name
Expand Down
Binary file modified Emby-DLL/Chronarr.Emby.Plugin.dll
Binary file not shown.
45 changes: 45 additions & 0 deletions core/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ def _init_postgresql_tables(self, cursor):
def upsert_series(self, imdb_id: str, path: str, instance: str = 'sonarr', metadata: Optional[Dict] = None):
with self.get_connection() as conn:
cursor = conn.cursor()
self._auto_migrate_series_on_path_match(cursor, imdb_id, path, instance)
timestamp = datetime.utcnow()
cursor.execute("""
INSERT INTO series (imdb_id, instance, path, last_updated, metadata)
Expand All @@ -337,6 +338,50 @@ def upsert_series(self, imdb_id: str, path: str, instance: str = 'sonarr', metad
last_updated = EXCLUDED.last_updated,
metadata = EXCLUDED.metadata
""", (imdb_id, instance, path, timestamp, json.dumps(metadata) if metadata else None))

def _auto_migrate_series_on_path_match(self, cursor, new_imdb_id: str, path: str, instance: str):
"""Detect Sonarr silently re-mapping a series' IMDb ID and merge instead of duplicating.

Common for very new/upcoming titles whose metadata hasn't settled on a
canonical IMDb ID yet - the next sync reports a different imdb_id for
the exact same show. series' PK is (imdb_id, instance), so without
this the upsert below would just create a second row for the "new" ID
and silently fork the episode history between the two. The series
path Sonarr hands us is the stable signal here (it doesn't change
just because a metadata ID got corrected), so a path match under the
same instance but a different imdb_id is treated as proof it's the
same show and merged via the same logic migrate_series_imdb_id() uses
manually - episodes move over, any season/episode that already exists
under the new ID (a genuine collision between the two, not just
happenstance) is dropped rather than left orphaned under a dead ID.
"""
if not path:
return
cursor.execute(
"SELECT imdb_id FROM series WHERE instance = %s AND path = %s AND imdb_id != %s",
(instance, path, new_imdb_id)
)
row = cursor.fetchone()
if not row:
return
old_imdb_id = row['imdb_id']
_log("INFO", f"[{instance}] Series IMDb ID changed: {old_imdb_id} -> {new_imdb_id} (same path: {path}) — auto-migrating episode history")

cursor.execute("""
UPDATE episodes SET imdb_id = %s, skipped = FALSE, skip_reason = NULL
WHERE imdb_id = %s AND instance = %s
AND NOT EXISTS (
SELECT 1 FROM episodes e2
WHERE e2.imdb_id = %s AND e2.instance = %s
AND e2.season = episodes.season AND e2.episode = episodes.episode
)
""", (new_imdb_id, old_imdb_id, instance, new_imdb_id, instance))

# Whatever's left under the old ID is a real season/episode collision
# with the new ID — can't move it without violating the PK, and the
# actively-syncing new ID's data is the one to trust.
cursor.execute("DELETE FROM episodes WHERE imdb_id = %s AND instance = %s", (old_imdb_id, instance))
cursor.execute("DELETE FROM series WHERE imdb_id = %s AND instance = %s", (old_imdb_id, instance))

def upsert_episode_date(self, imdb_id: str, season: int, episode: int,
aired: Optional[str], dateadded: Optional[str],
Expand Down
Loading