From 3db4b3279a01ffc72cce2c42fa61f40e389c1fa9 Mon Sep 17 00:00:00 2001 From: simonc56 Date: Wed, 20 May 2026 22:12:15 +0200 Subject: [PATCH 1/7] handle Trakt authentication errors with ClickException in watch command --- plextraktsync/commands/watch.py | 48 ++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/plextraktsync/commands/watch.py b/plextraktsync/commands/watch.py index d7d5309221..b3803f2f30 100644 --- a/plextraktsync/commands/watch.py +++ b/plextraktsync/commands/watch.py @@ -1,5 +1,8 @@ from __future__ import annotations +from click import ClickException +from trakt.errors import OAuthRefreshException + from plextraktsync.factory import factory from plextraktsync.watch.events import ( ActivityNotification, @@ -11,26 +14,29 @@ def watch(server: str): - factory.run_config.update( - server=server, - ) - ws = factory.web_socket_listener - updater = factory.watch_state_updater + try: + factory.run_config.update( + server=server, + ) + ws = factory.web_socket_listener + updater = factory.watch_state_updater - ws.on(ServerStarted, updater.on_start) - ws.on( - PlaySessionStateNotification, - updater.on_play, - state=["playing", "stopped", "paused"], - ) - ws.on( - ActivityNotification, - updater.on_activity, - type="library.refresh.items", - event="ended", - progress=100, - ) - ws.on(TimelineEntry, updater.on_delete, state=9, metadata_state="deleted") - ws.on(Error, updater.on_error) + ws.on(ServerStarted, updater.on_start) + ws.on( + PlaySessionStateNotification, + updater.on_play, + state=["playing", "stopped", "paused"], + ) + ws.on( + ActivityNotification, + updater.on_activity, + type="library.refresh.items", + event="ended", + progress=100, + ) + ws.on(TimelineEntry, updater.on_delete, state=9, metadata_state="deleted") + ws.on(Error, updater.on_error) - ws.listen() + ws.listen() + except OAuthRefreshException as e: + raise ClickException(f"Trakt error: Unable to refresh token: {e}") from e From ed7cde3ed342ad875f49a400e7a41ff0f88b64c2 Mon Sep 17 00:00:00 2001 From: simonc56 Date: Thu, 21 May 2026 21:06:47 +0200 Subject: [PATCH 2/7] Implement fatal error handling for OAuthRefreshException in watch websocket and background task --- plextraktsync/commands/watch.py | 1 + plextraktsync/factory/Factory.py | 10 ++++- plextraktsync/media/MediaFactory.py | 4 +- plextraktsync/queue/BackgroundTask.py | 15 +++++++- plextraktsync/watch/EventDispatcher.py | 9 ++++- plextraktsync/watch/FatalErrorState.py | 25 ++++++++++++ plextraktsync/watch/WebSocketListener.py | 10 ++++- tests/test_events.py | 15 ++++++++ tests/test_watch_fatal_error.py | 49 ++++++++++++++++++++++++ 9 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 plextraktsync/watch/FatalErrorState.py create mode 100644 tests/test_watch_fatal_error.py diff --git a/plextraktsync/commands/watch.py b/plextraktsync/commands/watch.py index b3803f2f30..f35471ac0c 100644 --- a/plextraktsync/commands/watch.py +++ b/plextraktsync/commands/watch.py @@ -15,6 +15,7 @@ def watch(server: str): try: + factory.watch_fatal_error.clear() factory.run_config.update( server=server, ) diff --git a/plextraktsync/factory/Factory.py b/plextraktsync/factory/Factory.py index 10bd6e8df5..e81c6d769b 100644 --- a/plextraktsync/factory/Factory.py +++ b/plextraktsync/factory/Factory.py @@ -211,7 +211,7 @@ def enable_self_update(self): def web_socket_listener(self): from plextraktsync.watch.WebSocketListener import WebSocketListener - return WebSocketListener(plex=self.plex_server) + return WebSocketListener(plex=self.plex_server, fatal_error=self.watch_fatal_error) @cached_property def watch_state_updater(self): @@ -224,6 +224,12 @@ def watch_state_updater(self): config=self.config, ) + @cached_property + def watch_fatal_error(self): + from plextraktsync.watch.FatalErrorState import FatalErrorState + + return FatalErrorState() + @cached_property def logging(self): import logging @@ -324,7 +330,7 @@ def queue(self): TraktMarkWatchedWorker(), TraktScrobbleWorker(), ] - task = BackgroundTask(self.batch_delay_timer, *workers) + task = BackgroundTask(self.batch_delay_timer, *workers, fatal_error=self.watch_fatal_error) queue = Queue(task) return queue diff --git a/plextraktsync/media/MediaFactory.py b/plextraktsync/media/MediaFactory.py index 38ca9d3b96..d0c32a2560 100644 --- a/plextraktsync/media/MediaFactory.py +++ b/plextraktsync/media/MediaFactory.py @@ -4,7 +4,7 @@ from plexapi.exceptions import PlexApiException from requests import RequestException -from trakt.errors import TraktException +from trakt.errors import OAuthRefreshException, TraktException from plextraktsync.factory import logging from plextraktsync.media.Media import Media @@ -66,6 +66,8 @@ def resolve_guid(self, guid: PlexGuid, show: Media = None): tm = self.trakt.find_episode_guid(guid, show.seasons) else: tm = self.trakt.find_by_guid(guid) + except OAuthRefreshException: + raise except (TraktException, RequestException) as e: self.logger.warning( f"{guid.title_link}: Skipping {guid}: Trakt errors: {e}", diff --git a/plextraktsync/queue/BackgroundTask.py b/plextraktsync/queue/BackgroundTask.py index b7f6455a74..a57b346547 100644 --- a/plextraktsync/queue/BackgroundTask.py +++ b/plextraktsync/queue/BackgroundTask.py @@ -4,6 +4,8 @@ from queue import Empty from typing import TYPE_CHECKING +from trakt.errors import OAuthRefreshException + from plextraktsync.factory import logging if TYPE_CHECKING: @@ -20,10 +22,11 @@ class BackgroundTask: logger = logging.getLogger(__name__) - def __init__(self, timer: Timer = None, *tasks): + def __init__(self, timer: Timer = None, *tasks, fatal_error=None): self.queues = defaultdict(list) self.timer = timer self.tasks = tasks + self.fatal_error = fatal_error def check_timer(self): if not self.timer: @@ -40,6 +43,10 @@ def timed_events(self): for task in self.tasks: try: task(self.queues) + except OAuthRefreshException as e: + if self.fatal_error is not None: + self.fatal_error.set(e) + return except Exception as e: self.logger.error(f"Got exception while working on {task}: {e}") @@ -60,6 +67,9 @@ def __call__(self, queue: SimpleQueue): """ while True: + if self.fatal_error is not None: + return self.fatal_error.raise_if_set() + try: message = queue.get(timeout=1) except Empty: @@ -71,3 +81,6 @@ def __call__(self, queue: SimpleQueue): self.process_message(message) self.check_timer() + + if self.fatal_error is not None: + return self.fatal_error.raise_if_set() diff --git a/plextraktsync/watch/EventDispatcher.py b/plextraktsync/watch/EventDispatcher.py index 315a38037d..c57995b80f 100644 --- a/plextraktsync/watch/EventDispatcher.py +++ b/plextraktsync/watch/EventDispatcher.py @@ -1,5 +1,7 @@ from __future__ import annotations +from trakt.errors import OAuthRefreshException + from plextraktsync.factory import logging from plextraktsync.watch.EventFactory import EventFactory from plextraktsync.watch.events import Error, ServerStarted @@ -8,9 +10,10 @@ class EventDispatcher: logger = logging.getLogger(__name__) - def __init__(self): + def __init__(self, fatal_error=None): self.event_listeners = [] self.event_factory = EventFactory() + self.fatal_error = fatal_error def on(self, event_type, listener, **kwargs): self.event_listeners.append( @@ -38,6 +41,10 @@ def dispatch(self, event): try: listener["listener"](event) + except OAuthRefreshException as e: + if self.fatal_error is not None: + self.fatal_error.set(e) + raise except Exception as e: self.logger.error(f"{type(e).__name__} was raised: {e}") diff --git a/plextraktsync/watch/FatalErrorState.py b/plextraktsync/watch/FatalErrorState.py new file mode 100644 index 0000000000..755044e416 --- /dev/null +++ b/plextraktsync/watch/FatalErrorState.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from threading import Lock + + +class FatalErrorState: + def __init__(self): + self._error = None + self._lock = Lock() + + def clear(self): + with self._lock: + self._error = None + + def set(self, error: Exception): + with self._lock: + if self._error is None: + self._error = error + + def raise_if_set(self): + with self._lock: + error = self._error + + if error is not None: + raise error diff --git a/plextraktsync/watch/WebSocketListener.py b/plextraktsync/watch/WebSocketListener.py index fe6a274e2b..060c8d65fc 100644 --- a/plextraktsync/watch/WebSocketListener.py +++ b/plextraktsync/watch/WebSocketListener.py @@ -14,11 +14,12 @@ class WebSocketListener: logger = logging.getLogger(__name__) - def __init__(self, plex: PlexServer, poll_interval=5, restart_interval=15): + def __init__(self, plex: PlexServer, poll_interval=5, restart_interval=15, fatal_error=None): self.plex = plex self.poll_interval = poll_interval self.restart_interval = restart_interval - self.dispatcher = EventDispatcher() + self.fatal_error = fatal_error + self.dispatcher = EventDispatcher(fatal_error=fatal_error) def on(self, event_type, listener, **kwargs): self.dispatcher.on(event_type, listener, **kwargs) @@ -26,11 +27,16 @@ def on(self, event_type, listener, **kwargs): def listen(self): self.logger.info("Listening for events!") while True: + if self.fatal_error is not None: + self.fatal_error.raise_if_set() + notifier = self.plex.startAlertListener(callback=self.dispatcher.event_handler) self.dispatcher.event_handler(ServerStarted(notifier=notifier)) while notifier.is_alive(): sleep(self.poll_interval) + if self.fatal_error is not None: + self.fatal_error.raise_if_set() self.dispatcher.event_handler(Error(msg="Server closed connection")) self.logger.error(f"Listener finished. Restarting in {self.restart_interval} seconds") diff --git a/tests/test_events.py b/tests/test_events.py index 4a2c6aef73..5ccf980eab 100755 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -1,9 +1,13 @@ #!/usr/bin/env python3 -m pytest from __future__ import annotations +import pytest +from trakt.errors import OAuthRefreshException + from plextraktsync.watch.EventDispatcher import EventDispatcher from plextraktsync.watch.EventFactory import EventFactory from plextraktsync.watch.events import ActivityNotification +from plextraktsync.watch.FatalErrorState import FatalErrorState from tests.conftest import load_mock @@ -73,3 +77,14 @@ def test_event_dispatcher(): dispatcher = EventDispatcher().on(ActivityNotification, lambda x: events.append(x), event=["ended"], progress=99) dispatcher.event_handler(raw_events[4]) assert len(events) == 0, "No match for event=ended and progress=99" + + +def test_event_dispatcher_reraises_oauth_refresh_exception(): + fatal_error = FatalErrorState() + dispatcher = EventDispatcher(fatal_error=fatal_error).on(ActivityNotification, lambda _: (_ for _ in ()).throw(OAuthRefreshException())) + + with pytest.raises(OAuthRefreshException): + dispatcher.dispatch(ActivityNotification(Activity={"event": "ended", "type": "library.refresh.items", "progress": 100, "Context": {"key": "/library/metadata/1"}}, event="ended")) + + with pytest.raises(OAuthRefreshException): + fatal_error.raise_if_set() diff --git a/tests/test_watch_fatal_error.py b/tests/test_watch_fatal_error.py new file mode 100644 index 0000000000..9048b3664d --- /dev/null +++ b/tests/test_watch_fatal_error.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 -m pytest +from __future__ import annotations + +import pytest +from trakt.errors import OAuthRefreshException + +from plextraktsync.queue.BackgroundTask import BackgroundTask +from plextraktsync.watch.FatalErrorState import FatalErrorState +from plextraktsync.watch.WebSocketListener import WebSocketListener + + +def test_background_task_records_oauth_refresh_exception(): + fatal_error = FatalErrorState() + + def task(_queues): + raise OAuthRefreshException() + + background_task = BackgroundTask(None, task, fatal_error=fatal_error) + background_task.timed_events() + + with pytest.raises(OAuthRefreshException): + fatal_error.raise_if_set() + + +def test_websocket_listener_raises_recorded_oauth_refresh_exception(monkeypatch): + fatal_error = FatalErrorState() + + class Notifier: + def __init__(self): + self.calls = 0 + + def is_alive(self): + self.calls += 1 + return self.calls == 1 + + class Plex: + def startAlertListener(self, callback): + self.callback = callback + return Notifier() + + def fail_sleep(_interval): + fatal_error.set(OAuthRefreshException()) + + monkeypatch.setattr("plextraktsync.watch.WebSocketListener.sleep", fail_sleep) + + listener = WebSocketListener(Plex(), poll_interval=0, fatal_error=fatal_error) + + with pytest.raises(OAuthRefreshException): + listener.listen() \ No newline at end of file From 046fbefd6d2c1a3e443f481d3be824fbf9269acf Mon Sep 17 00:00:00 2001 From: simonc56 Date: Thu, 21 May 2026 22:19:32 +0200 Subject: [PATCH 3/7] fix return statement --- plextraktsync/queue/BackgroundTask.py | 7 ++++--- tests/test_watch_fatal_error.py | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/plextraktsync/queue/BackgroundTask.py b/plextraktsync/queue/BackgroundTask.py index a57b346547..788e318304 100644 --- a/plextraktsync/queue/BackgroundTask.py +++ b/plextraktsync/queue/BackgroundTask.py @@ -46,7 +46,8 @@ def timed_events(self): except OAuthRefreshException as e: if self.fatal_error is not None: self.fatal_error.set(e) - return + return + raise except Exception as e: self.logger.error(f"Got exception while working on {task}: {e}") @@ -68,7 +69,7 @@ def __call__(self, queue: SimpleQueue): while True: if self.fatal_error is not None: - return self.fatal_error.raise_if_set() + self.fatal_error.raise_if_set() try: message = queue.get(timeout=1) @@ -83,4 +84,4 @@ def __call__(self, queue: SimpleQueue): self.check_timer() if self.fatal_error is not None: - return self.fatal_error.raise_if_set() + self.fatal_error.raise_if_set() diff --git a/tests/test_watch_fatal_error.py b/tests/test_watch_fatal_error.py index 9048b3664d..05b15e668e 100644 --- a/tests/test_watch_fatal_error.py +++ b/tests/test_watch_fatal_error.py @@ -22,6 +22,29 @@ def task(_queues): fatal_error.raise_if_set() +def test_background_task_continues_without_fatal_error(): + fatal_error = FatalErrorState() + calls = [] + + class Queue: + def __init__(self): + self.messages = iter([ + ("test", 1), + None, + ]) + + def get(self, timeout): + return next(self.messages) + + def task(queues): + calls.append(list(queues["test"])) + + background_task = BackgroundTask(None, task, fatal_error=fatal_error) + background_task(Queue()) + + assert calls == [[1]] + + def test_websocket_listener_raises_recorded_oauth_refresh_exception(monkeypatch): fatal_error = FatalErrorState() From b2c0bad2694b0b0d4d97ef37eb859305c436c333 Mon Sep 17 00:00:00 2001 From: simonc56 Date: Thu, 21 May 2026 23:13:16 +0200 Subject: [PATCH 4/7] fix tests with OAuthRefreshException --- tests/conftest.py | 15 +++++++++++++++ tests/test_events.py | 19 ++++++++++++++++--- tests/test_watch_fatal_error.py | 5 +++-- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 6179cef285..17aa275e7b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,7 @@ from os.path import dirname from os.path import join as join_path +from trakt.errors import OAuthRefreshException from trakt.tv import TVShow from plextraktsync.factory import Factory @@ -35,3 +36,17 @@ def make(cls=None, **kwargs) -> TVShow: cls = cls if cls is not None else "object" # https://stackoverflow.com/a/2827726/2314626 return type(cls, (object,), kwargs) + + +def make_oauth_refresh_exception( + error: str = "invalid_grant", + error_description: str = "The provided authorization grant is invalid.", +) -> OAuthRefreshException: + response = make( + cls="Response", + json=lambda self: { + "error": error, + "error_description": error_description, + }, + )() + return OAuthRefreshException(response) diff --git a/tests/test_events.py b/tests/test_events.py index 5ccf980eab..a35a24a7e4 100755 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -8,7 +8,7 @@ from plextraktsync.watch.EventFactory import EventFactory from plextraktsync.watch.events import ActivityNotification from plextraktsync.watch.FatalErrorState import FatalErrorState -from tests.conftest import load_mock +from tests.conftest import load_mock, make_oauth_refresh_exception def test_events(): @@ -81,10 +81,23 @@ def test_event_dispatcher(): def test_event_dispatcher_reraises_oauth_refresh_exception(): fatal_error = FatalErrorState() - dispatcher = EventDispatcher(fatal_error=fatal_error).on(ActivityNotification, lambda _: (_ for _ in ()).throw(OAuthRefreshException())) + dispatcher = EventDispatcher(fatal_error=fatal_error).on( + ActivityNotification, + lambda _: (_ for _ in ()).throw(make_oauth_refresh_exception()), + ) with pytest.raises(OAuthRefreshException): - dispatcher.dispatch(ActivityNotification(Activity={"event": "ended", "type": "library.refresh.items", "progress": 100, "Context": {"key": "/library/metadata/1"}}, event="ended")) + dispatcher.dispatch( + ActivityNotification( + Activity={ + "event": "ended", + "type": "library.refresh.items", + "progress": 100, + "Context": {"key": "/library/metadata/1"}, + }, + event="ended", + ) + ) with pytest.raises(OAuthRefreshException): fatal_error.raise_if_set() diff --git a/tests/test_watch_fatal_error.py b/tests/test_watch_fatal_error.py index 05b15e668e..4febd27325 100644 --- a/tests/test_watch_fatal_error.py +++ b/tests/test_watch_fatal_error.py @@ -7,13 +7,14 @@ from plextraktsync.queue.BackgroundTask import BackgroundTask from plextraktsync.watch.FatalErrorState import FatalErrorState from plextraktsync.watch.WebSocketListener import WebSocketListener +from tests.conftest import make_oauth_refresh_exception def test_background_task_records_oauth_refresh_exception(): fatal_error = FatalErrorState() def task(_queues): - raise OAuthRefreshException() + raise make_oauth_refresh_exception() background_task = BackgroundTask(None, task, fatal_error=fatal_error) background_task.timed_events() @@ -62,7 +63,7 @@ def startAlertListener(self, callback): return Notifier() def fail_sleep(_interval): - fatal_error.set(OAuthRefreshException()) + fatal_error.set(make_oauth_refresh_exception()) monkeypatch.setattr("plextraktsync.watch.WebSocketListener.sleep", fail_sleep) From c32b6892ce6c6ec46a8a1f49faabb174702d595c Mon Sep 17 00:00:00 2001 From: simonc56 Date: Thu, 28 May 2026 20:23:53 +0200 Subject: [PATCH 5/7] re-raise ClickException in cli.py so that process exits with code 1 --- plextraktsync/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plextraktsync/cli.py b/plextraktsync/cli.py index 36bc630654..8d6a75d275 100644 --- a/plextraktsync/cli.py +++ b/plextraktsync/cli.py @@ -33,6 +33,7 @@ def wrap(*args, **kwargs): logger = logging.getLogger(__name__) logger.fatal(f"Error running {name} command: {str(e)}") + raise except Exception as e: from plextraktsync.factory import logging From d95bbf514fb6ff03699193093ebf2de19aaabe8b Mon Sep 17 00:00:00 2001 From: simonc56 Date: Thu, 28 May 2026 22:29:17 +0200 Subject: [PATCH 6/7] refactor EventDispatcher and WebSocketListener to remove fatal_error handling and streamline OAuthRefreshException --- plextraktsync/watch/EventDispatcher.py | 7 ++----- plextraktsync/watch/WebSocketListener.py | 18 ++++++++++++++---- tests/test_events.py | 7 +------ 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/plextraktsync/watch/EventDispatcher.py b/plextraktsync/watch/EventDispatcher.py index c57995b80f..59a09fe5ff 100644 --- a/plextraktsync/watch/EventDispatcher.py +++ b/plextraktsync/watch/EventDispatcher.py @@ -10,10 +10,9 @@ class EventDispatcher: logger = logging.getLogger(__name__) - def __init__(self, fatal_error=None): + def __init__(self): self.event_listeners = [] self.event_factory = EventFactory() - self.fatal_error = fatal_error def on(self, event_type, listener, **kwargs): self.event_listeners.append( @@ -41,9 +40,7 @@ def dispatch(self, event): try: listener["listener"](event) - except OAuthRefreshException as e: - if self.fatal_error is not None: - self.fatal_error.set(e) + except OAuthRefreshException: raise except Exception as e: self.logger.error(f"{type(e).__name__} was raised: {e}") diff --git a/plextraktsync/watch/WebSocketListener.py b/plextraktsync/watch/WebSocketListener.py index 060c8d65fc..fe0df7c9d4 100644 --- a/plextraktsync/watch/WebSocketListener.py +++ b/plextraktsync/watch/WebSocketListener.py @@ -3,6 +3,8 @@ from time import sleep from typing import TYPE_CHECKING +from trakt.errors import OAuthRefreshException + from plextraktsync.factory import logging from plextraktsync.watch.EventDispatcher import EventDispatcher from plextraktsync.watch.events import Error, ServerStarted @@ -19,25 +21,33 @@ def __init__(self, plex: PlexServer, poll_interval=5, restart_interval=15, fatal self.poll_interval = poll_interval self.restart_interval = restart_interval self.fatal_error = fatal_error - self.dispatcher = EventDispatcher(fatal_error=fatal_error) + self.dispatcher = EventDispatcher() def on(self, event_type, listener, **kwargs): self.dispatcher.on(event_type, listener, **kwargs) + def event_handler(self, data): + try: + return self.dispatcher.event_handler(data) + except OAuthRefreshException as e: + if self.fatal_error is not None: + self.fatal_error.set(e) + raise + def listen(self): self.logger.info("Listening for events!") while True: if self.fatal_error is not None: self.fatal_error.raise_if_set() - notifier = self.plex.startAlertListener(callback=self.dispatcher.event_handler) - self.dispatcher.event_handler(ServerStarted(notifier=notifier)) + notifier = self.plex.startAlertListener(callback=self.event_handler) + self.event_handler(ServerStarted(notifier=notifier)) while notifier.is_alive(): sleep(self.poll_interval) if self.fatal_error is not None: self.fatal_error.raise_if_set() - self.dispatcher.event_handler(Error(msg="Server closed connection")) + self.event_handler(Error(msg="Server closed connection")) self.logger.error(f"Listener finished. Restarting in {self.restart_interval} seconds") sleep(self.restart_interval) diff --git a/tests/test_events.py b/tests/test_events.py index a35a24a7e4..37ff037b7a 100755 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -7,7 +7,6 @@ from plextraktsync.watch.EventDispatcher import EventDispatcher from plextraktsync.watch.EventFactory import EventFactory from plextraktsync.watch.events import ActivityNotification -from plextraktsync.watch.FatalErrorState import FatalErrorState from tests.conftest import load_mock, make_oauth_refresh_exception @@ -80,8 +79,7 @@ def test_event_dispatcher(): def test_event_dispatcher_reraises_oauth_refresh_exception(): - fatal_error = FatalErrorState() - dispatcher = EventDispatcher(fatal_error=fatal_error).on( + dispatcher = EventDispatcher().on( ActivityNotification, lambda _: (_ for _ in ()).throw(make_oauth_refresh_exception()), ) @@ -98,6 +96,3 @@ def test_event_dispatcher_reraises_oauth_refresh_exception(): event="ended", ) ) - - with pytest.raises(OAuthRefreshException): - fatal_error.raise_if_set() From 83fde48077f711a77c3c5b661891dc5703381896 Mon Sep 17 00:00:00 2001 From: simonc56 Date: Thu, 28 May 2026 22:42:54 +0200 Subject: [PATCH 7/7] handle fatal error in BackgroundTask call method to ensure proper shutdown --- plextraktsync/queue/BackgroundTask.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plextraktsync/queue/BackgroundTask.py b/plextraktsync/queue/BackgroundTask.py index 788e318304..9558217ceb 100644 --- a/plextraktsync/queue/BackgroundTask.py +++ b/plextraktsync/queue/BackgroundTask.py @@ -78,6 +78,8 @@ def __call__(self, queue: SimpleQueue): else: if message is None: self.shutdown() + if self.fatal_error is not None: + self.fatal_error.raise_if_set() break self.process_message(message)