diff --git a/README.md b/README.md index 3f190fa..fc94e69 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,10 @@ plexautolanguages: # It is recommended to disable this parameter if you have a large TV Show library (10k+ episodes) refresh_library_on_scan: true + # PlexAutoLanguages will ignore shows with any of the following Plex tags + ignore_tags: + - PAL_IGNORE + # Plex configuration plex: # A valid Plex URL (required) diff --git a/config/default.yaml b/config/default.yaml index 25103cb..9ea245f 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -5,6 +5,8 @@ plexautolanguages: trigger_on_scan: true trigger_on_activity: false refresh_library_on_scan: true + ignore_tags: + - PAL_IGNORE plex: url: "" diff --git a/plex_auto_languages/alerts/activity.py b/plex_auto_languages/alerts/activity.py index 9c17c84..899cad6 100644 --- a/plex_auto_languages/alerts/activity.py +++ b/plex_auto_languages/alerts/activity.py @@ -58,6 +58,11 @@ def process(self, plex: PlexServer): if item is None or not isinstance(item, Episode): return + # Skip if the show should be ignored + if plex.should_ignore_show(item.show()): + logger.debug(f"[Activity] Ignoring episode {item} due to Plex show tags") + return + # Skip if this item has already been seen in the last 3 seconds activity_key = (self.user_id, self.item_key) if activity_key in plex.cache.recent_activities and \ diff --git a/plex_auto_languages/alerts/playing.py b/plex_auto_languages/alerts/playing.py index f20ebbd..1f01c08 100644 --- a/plex_auto_languages/alerts/playing.py +++ b/plex_auto_languages/alerts/playing.py @@ -51,6 +51,11 @@ def process(self, plex: PlexServer): if item is None or not isinstance(item, Episode): return + # Skip if the show should be ignored + if plex.should_ignore_show(item.show()): + logger.debug(f"[Play Session] Ignoring episode {item} due to Plex show tags") + return + # Skip is the session state is unchanged if self.session_key in plex.cache.session_states and plex.cache.session_states[self.session_key] == self.session_state: return diff --git a/plex_auto_languages/alerts/status.py b/plex_auto_languages/alerts/status.py index a7f20c6..19c6109 100644 --- a/plex_auto_languages/alerts/status.py +++ b/plex_auto_languages/alerts/status.py @@ -35,6 +35,10 @@ def process(self, plex: PlexServer): if len(added) > 0: logger.debug(f"[Status] Found {len(added)} newly added episode(s)") for item in added: + # Check if the item should be ignored + if plex.should_ignore_show(item.show()): + continue + # Check if the item has already been processed if not plex.cache.should_process_recently_added(item.key, item.addedAt): continue @@ -47,6 +51,10 @@ def process(self, plex: PlexServer): if len(updated) > 0: logger.debug(f"[Status] Found {len(updated)} updated episode(s)") for item in updated: + # Check if the item should be ignored + if plex.should_ignore_show(item.show()): + continue + # Check if the item has already been processed if not plex.cache.should_process_recently_updated(item.key): continue diff --git a/plex_auto_languages/alerts/timeline.py b/plex_auto_languages/alerts/timeline.py index cb229bd..03c3e28 100644 --- a/plex_auto_languages/alerts/timeline.py +++ b/plex_auto_languages/alerts/timeline.py @@ -53,6 +53,11 @@ def process(self, plex: PlexServer): if item is None or not isinstance(item, Episode): return + # Skip if the show should be ignored + if plex.should_ignore_show(item.show()): + logger.debug(f"[Timeline] Ignoring episode {item} due to Plex show tags") + return + # Check if the item has been added recently if item.addedAt < datetime.now() - timedelta(minutes=5): return diff --git a/plex_auto_languages/plex_server.py b/plex_auto_languages/plex_server.py index 9c3adde..395ce0c 100644 --- a/plex_auto_languages/plex_server.py +++ b/plex_auto_languages/plex_server.py @@ -213,6 +213,12 @@ def get_user_by_id(self, user_id: Union[int, str]): return None return matching_users[0] + def should_ignore_show(self, show: Show): + for label in show.labels: + if label.tag and label.tag in self.config.get("ignore_tags"): + return True + return False + def process_new_or_updated_episode(self, item_id: Union[int, str], event_type: EventType, new: bool): track_changes = NewOrUpdatedTrackChanges(event_type, new) for user_id in self.get_all_user_ids(): @@ -280,11 +286,15 @@ def start_deep_analysis(self): # Scan library added, updated = self.cache.refresh_library_cache() for item in added: + if self.should_ignore_show(item.show()): + continue if not self.cache.should_process_recently_added(item.key, item.addedAt): continue logger.info(f"[Scheduler] Processing newly added episode {self.get_episode_short_name(item)}") self.process_new_or_updated_episode(item.key, EventType.SCHEDULER, True) for item in updated: + if self.should_ignore_show(item.show()): + continue if not self.cache.should_process_recently_updated(item.key): continue logger.info(f"[Scheduler] Processing updated episode {self.get_episode_short_name(item)}") diff --git a/plex_auto_languages/utils/configuration.py b/plex_auto_languages/utils/configuration.py index 6fce8d0..b21af0f 100644 --- a/plex_auto_languages/utils/configuration.py +++ b/plex_auto_languages/utils/configuration.py @@ -71,6 +71,7 @@ def __init__(self, user_config_path: str): self._override_from_config_file(user_config_path) self._override_from_env() self._override_plex_token_from_secret() + self._postprocess_config() self._validate_config() self._add_system_config() if self.get("debug"): @@ -104,6 +105,11 @@ def _override_plex_token_from_secret(self): plex_token = stream.readline().strip() self._config["plex"]["token"] = plex_token + def _postprocess_config(self): + ignore_tags_config = self.get("ignore_tags") + if isinstance(ignore_tags_config, str): + self._config["ignore_tags"] = ignore_tags_config.split(",") + def _validate_config(self): if self.get("plex.url") == "": logger.error("A Plex URL is required") @@ -117,6 +123,9 @@ def _validate_config(self): if self.get("update_strategy") not in ["all", "next"]: logger.error("The 'update_strategy' parameter must be either 'all' or 'next'") raise InvalidConfiguration + if not isinstance(self.get("ignore_tags"), list): + logger.error("The 'ignore_tags' parameter must be a list or a string-based comma separated list") + raise InvalidConfiguration if self.get("scheduler.enable") and not re.match(r"^\d{2}:\d{2}$", self.get("scheduler.schedule_time")): logger.error("A valid 'schedule_time' parameter with the format 'HH:MM' is required (ex: 02:30)") raise InvalidConfiguration diff --git a/tests/test_alerts_activity.py b/tests/test_alerts_activity.py index 78890cd..053b580 100644 --- a/tests/test_alerts_activity.py +++ b/tests/test_alerts_activity.py @@ -43,6 +43,16 @@ def test_activity(plex, episode): mocked_change_tracks.assert_called_once_with(plex.username, episode, EventType.PLAY_OR_ACTIVITY) plex.cache.recent_activities.clear() + # Not called because the show is ignored + mocked_change_tracks.reset_mock() + plex.cache.recent_activities.clear() + plex.config._config["ignore_tags"] = ["PAL_IGNORE"] + episode.show().addLabel("PAL_IGNORE") + activity.process(plex) + mocked_change_tracks.assert_not_called() + episode.show().removeLabel("PAL_IGNORE") + activity._message = copy.deepcopy(activity_message) + # Not called because the event is 'started' mocked_change_tracks.reset_mock() activity._message["event"] = "started" diff --git a/tests/test_alerts_playing.py b/tests/test_alerts_playing.py index 79452f7..0efd4a0 100644 --- a/tests/test_alerts_playing.py +++ b/tests/test_alerts_playing.py @@ -26,7 +26,16 @@ def test_playing(plex, episode): plex.cache.user_clients["some_identifier"] = (plex.user_id, plex.username) with patch.object(PlexServer, "change_tracks") as mocked_change_tracks: + # Not called because the show is ignored + mocked_change_tracks.reset_mock() + plex.config._config["ignore_tags"] = ["PAL_IGNORE"] + episode.show().addLabel("PAL_IGNORE") + playing.process(plex) + mocked_change_tracks.assert_not_called() + episode.show().removeLabel("PAL_IGNORE") + # Default behavior + mocked_change_tracks.reset_mock() playing.process(plex) mocked_change_tracks.assert_called_once_with(plex.username, episode, EventType.PLAY_OR_ACTIVITY) plex.cache.default_streams.clear() diff --git a/tests/test_alerts_status.py b/tests/test_alerts_status.py index e54e203..80f7ee3 100644 --- a/tests/test_alerts_status.py +++ b/tests/test_alerts_status.py @@ -14,11 +14,20 @@ def test_status(plex, episode): status = PlexStatus(copy.deepcopy(status_message)) assert status.title == "Library scan complete" + plex.config._config["ignore_tags"] = ["PAL_IGNORE"] + with patch.object(PlexServer, "process_new_or_updated_episode") as mocked_process: plex.config._config["refresh_library_on_scan"] = True with patch.object(PlexServerCache, "refresh_library_cache", return_value=([episode], [])): + # Not called because the show should be ignored + mocked_process.reset_mock() + episode.show().addLabel("PAL_IGNORE") + status.process(plex) + mocked_process.assert_not_called() + episode.show().removeLabel("PAL_IGNORE") + # Default behavior for new episode mocked_process.reset_mock() status.process(plex) @@ -31,6 +40,13 @@ def test_status(plex, episode): plex.cache.newly_added.clear() with patch.object(PlexServerCache, "refresh_library_cache", return_value=([], [episode])): + # Not called because the show should be ignored + mocked_process.reset_mock() + episode.show().addLabel("PAL_IGNORE") + status.process(plex) + mocked_process.assert_not_called() + episode.show().removeLabel("PAL_IGNORE") + # Default behavior for updated episode mocked_process.reset_mock() status.process(plex) diff --git a/tests/test_alerts_timeline.py b/tests/test_alerts_timeline.py index 77b5eae..72268b9 100644 --- a/tests/test_alerts_timeline.py +++ b/tests/test_alerts_timeline.py @@ -21,11 +21,21 @@ def test_timeline(plex, episode): assert timeline.state == 5 assert timeline.entry_type == 0 + plex.config._config["ignore_tags"] = ["PAL_IGNORE"] + with patch.object(PlexServer, "process_new_or_updated_episode") as mocked_process: fake_recent_episode = copy.deepcopy(episode) fake_recent_episode.addedAt = datetime.now() with patch.object(PlexServer, "fetch_item", return_value=fake_recent_episode): + # Not called because the show should be ignored + mocked_process.reset_mock() + episode.show().addLabel("PAL_IGNORE") + timeline.process(plex) + mocked_process.assert_not_called() + episode.show().removeLabel("PAL_IGNORE") + # Default behavior + mocked_process.reset_mock() timeline.process(plex) mocked_process.assert_called_once_with(item_id, EventType.NEW_EPISODE, True) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index c7a26c0..50cbf6d 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -110,7 +110,8 @@ def test_configuration_user_config(): "plex": { "url": "http://localhost:32400", "token": "token" - } + }, + "ignore_tags": "TAG_1,TAG_2" } } fd, path = tempfile.mkstemp() @@ -120,6 +121,10 @@ def test_configuration_user_config(): config = Configuration(path) assert config.get("plex.url") == "http://localhost:32400" assert config.get("plex.token") == "token" + assert isinstance(config.get("ignore_tags"), list) + assert len(config.get("ignore_tags")) == 2 + assert "TAG_1" in config.get("ignore_tags") + assert "TAG_2" in config.get("ignore_tags") finally: os.remove(path) @@ -175,6 +180,20 @@ def test_configuration_unvalidated(): _ = Configuration(None) del os.environ["UPDATE_STRATEGY"] + config_dict = { + "plexautolanguages": { + "ignore_tags": 12 + } + } + fd, path = tempfile.mkstemp() + try: + with open(fd, "w", encoding="utf-8") as stream: + yaml.dump(config_dict, stream) + with pytest.raises(InvalidConfiguration): + _ = Configuration(path) + finally: + os.remove(path) + os.environ["SCHEDULER_ENABLE"] = "true" os.environ["SCHEDULER_SCHEDULE_TIME"] = "12h30" with pytest.raises(InvalidConfiguration): diff --git a/tests/test_plex_server.py b/tests/test_plex_server.py index 4859524..cc0de5d 100644 --- a/tests/test_plex_server.py +++ b/tests/test_plex_server.py @@ -122,6 +122,16 @@ def test_get_user_by_id(plex): assert user is None +def test_should_ignore_show(plex, episode): + plex.config._config["ignore_tags"] = ["PAL_IGNORE"] + + episode.show().addLabel("PAL_IGNORE") + assert plex.should_ignore_show(episode.show()) is True + + episode.show().removeLabel("PAL_IGNORE") + assert plex.should_ignore_show(episode.show()) is False + + def test_get_all_user_ids(plex): user_ids = plex.get_all_user_ids() assert len(user_ids) == 2