diff --git a/README.md b/README.md index f76dff9..d56ea8b 100644 --- a/README.md +++ b/README.md @@ -136,8 +136,13 @@ plexautolanguages: # A notification is sent whenever a language change is performed enable: true # An array of Apprise configurations, see Apprise docs for more information: https://github.com/caronc/apprise + # The array 'users' can be specified in order to link notification URLs with specific users + # Defaults to all users if not present + # The array 'events' can be specified in order to get notifications only for specific events + # Valid event values: "play_or_activity" "new_episode" "updated_episode" "scheduler" + # Defaults to all events if not present apprise_configs: - # This URL will be notified of all changes + # This URL will be notified of all changes during all events - "discord://webhook_id/webhook_token" # These URLs will only be notified of language change for users "MyUser1" and "MyUser2" - urls: @@ -146,9 +151,18 @@ plexautolanguages: users: - "MyUser1" - "MyUser2" - # This URL will only be notified of language change for user "MyUser3" - - urls: "tgram://bottoken/ChatID" - users: "MyUser3" + # This URL will only be notified of language change for user "MyUser3" during play or activity events + - urls: + - "tgram://bottoken/ChatID" + users: + - "MyUser3" + events: + - "play_or_activity" + # This URL will be notified of language change during scheduler tasks only + - urls: + - "gotify://hostname/token" + events: + - "scheduler" - "..." # Whether or not to enable the debug mode, defaults to 'false' diff --git a/plex_auto_languages/alerts/activity.py b/plex_auto_languages/alerts/activity.py index 6f3bda1..9c17c84 100644 --- a/plex_auto_languages/alerts/activity.py +++ b/plex_auto_languages/alerts/activity.py @@ -5,6 +5,7 @@ from plex_auto_languages.alerts.base import PlexAlert from plex_auto_languages.utils.logger import get_logger +from plex_auto_languages.constants import EventType if TYPE_CHECKING: from plex_auto_languages.plex_server import PlexServer @@ -70,4 +71,4 @@ def process(self, plex: PlexServer): if user is None: return logger.debug(f"[Activity] User: {user.name} | Episode: {item}") - plex.change_default_tracks_if_needed(user.name, item) + plex.change_tracks(user.name, item, EventType.PLAY_OR_ACTIVITY) diff --git a/plex_auto_languages/alerts/playing.py b/plex_auto_languages/alerts/playing.py index f9e12cc..8bd1be1 100644 --- a/plex_auto_languages/alerts/playing.py +++ b/plex_auto_languages/alerts/playing.py @@ -4,6 +4,7 @@ from plex_auto_languages.alerts.base import PlexAlert from plex_auto_languages.utils.logger import get_logger +from plex_auto_languages.constants import EventType if TYPE_CHECKING: from plex_auto_languages.plex_server import PlexServer @@ -73,4 +74,4 @@ def process(self, plex: PlexServer): plex.cache.default_streams.setdefault(item.key, pair_id) # Change tracks if needed - plex.change_default_tracks_if_needed(username, item) + plex.change_tracks(username, item, EventType.PLAY_OR_ACTIVITY) diff --git a/plex_auto_languages/alerts/status.py b/plex_auto_languages/alerts/status.py index 7f4281e..0791feb 100644 --- a/plex_auto_languages/alerts/status.py +++ b/plex_auto_languages/alerts/status.py @@ -3,6 +3,7 @@ from plex_auto_languages.alerts.base import PlexAlert from plex_auto_languages.utils.logger import get_logger +from plex_auto_languages.constants import EventType if TYPE_CHECKING: from plex_auto_languages.plex_server import PlexServer @@ -37,7 +38,7 @@ def process(self, plex: PlexServer): # Change tracks for all users logger.info(f"[Status] Processing newly added episode {plex.get_episode_short_name(item)}") - plex.process_new_or_updated_episode(item.key) + plex.process_new_or_updated_episode(item.key, EventType.NEW_EPISODE) # Process updated episodes if len(updated) > 0: @@ -45,4 +46,4 @@ def process(self, plex: PlexServer): for item in updated: # Change tracks for all users logger.info(f"[Status] Processing updated episode {plex.get_episode_short_name(item)}") - plex.process_new_or_updated_episode(item.key, new=False) + plex.process_new_or_updated_episode(item.key, EventType.UPDATED_EPISODE) diff --git a/plex_auto_languages/alerts/timeline.py b/plex_auto_languages/alerts/timeline.py index 7a5dcd3..153689c 100644 --- a/plex_auto_languages/alerts/timeline.py +++ b/plex_auto_languages/alerts/timeline.py @@ -5,6 +5,7 @@ from plex_auto_languages.alerts.base import PlexAlert from plex_auto_languages.utils.logger import get_logger +from plex_auto_languages.constants import EventType if TYPE_CHECKING: from plex_auto_languages.plex_server import PlexServer @@ -60,4 +61,4 @@ def process(self, plex: PlexServer): # Change tracks for all users logger.info(f"[Timeline] Processing newly added episode {plex.get_episode_short_name(item)}") - plex.process_new_or_updated_episode(self.item_id) + plex.process_new_or_updated_episode(self.item_id, EventType.NEW_EPISODE) diff --git a/plex_auto_languages/constants.py b/plex_auto_languages/constants.py new file mode 100644 index 0000000..9d25f7b --- /dev/null +++ b/plex_auto_languages/constants.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class EventType(Enum): + + PLAY_OR_ACTIVITY = 0 + NEW_EPISODE = 1 + UPDATED_EPISODE = 2 + SCHEDULER = 3 diff --git a/plex_auto_languages/plex_server.py b/plex_auto_languages/plex_server.py index baed402..43bfd35 100644 --- a/plex_auto_languages/plex_server.py +++ b/plex_auto_languages/plex_server.py @@ -1,6 +1,6 @@ import sys import itertools -from typing import List, Union +from typing import Union from datetime import datetime, timedelta from plexapi.media import MediaPart from plexapi.library import ShowSection @@ -11,9 +11,10 @@ from plex_auto_languages.utils.logger import get_logger from plex_auto_languages.utils.configuration import Configuration from plex_auto_languages.plex_alert_handler import PlexAlertHandler -from plex_auto_languages.track_changes import TrackChanges +from plex_auto_languages.track_changes import TrackChanges, NewOrUpdatedTrackChanges from plex_auto_languages.utils.notifier import Notifier from plex_auto_languages.plex_server_cache import PlexServerCache +from plex_auto_languages.constants import EventType logger = get_logger() @@ -136,7 +137,8 @@ def get_user_by_id(self, user_id: Union[int, str]): return None return matching_users[0] - def process_new_or_updated_episode(self, item_id: Union[int, str], new: bool = True): + def process_new_or_updated_episode(self, item_id: Union[int, str], event_type: EventType): + track_changes = NewOrUpdatedTrackChanges(event_type) for user_id in self.get_all_user_ids(): # Switch to the user's Plex instance user_plex = self.get_plex_instance_of_user(user_id) @@ -157,53 +159,39 @@ def process_new_or_updated_episode(self, item_id: Union[int, str], new: bool = T user = self.get_user_by_id(user_id) if user is None: return - self.change_default_tracks_if_needed(user.name, reference, episodes=[user_item], notify=False) - self.notify_updated_or_new_episode(self.fetch_item(item_id), new) - - def change_default_tracks_if_needed(self, username: str, episode: Episode, episodes: List[Episode] = None, - notify: bool = True): - track_changes = TrackChanges(username, episode) - logger.debug(f"[Language Update] " - f"Checking language update for show {episode.show()} and user '{username}' based on episode {episode}") - if episodes is None: - # Get episodes to update - episodes = track_changes.get_episodes_to_update( - self.config.get("update_level"), self.config.get("update_strategy")) + track_changes.change_track_for_user(user.name, reference, user_item) + + # Notify changes + if track_changes.has_changes: + self.notify_changes(track_changes) + + def change_tracks(self, username: str, episode: Episode, event_type: EventType): + track_changes = TrackChanges(username, episode, event_type) + # Get episodes to update + episodes = track_changes.get_episodes_to_update(self.config.get("update_level"), self.config.get("update_strategy")) # Get changes to perform track_changes.compute(episodes) - if not track_changes.has_changes: - logger.debug(f"[Language Update] No changes to perform for show {episode.show()} and user '{username}'") - return False # Perform changes track_changes.apply() # Notify changes - if notify: + if track_changes.has_changes: self.notify_changes(track_changes) - return True - def notify_changes(self, track_changes: TrackChanges): + def notify_changes(self, track_changes: Union[TrackChanges, NewOrUpdatedTrackChanges]): logger.info(f"Language update: {track_changes.inline_description}") if self.notifier is None: return - title = f"PlexAutoLanguages - {track_changes.reference_name}" - self.notifier.notify_user(title, track_changes.description, track_changes.username) - - def notify_updated_or_new_episode(self, episode: Episode, new: bool): - title = f"PlexAutoLanguages - {'New' if new else 'Updated'} episode" - message = ( - f"Episode: {self.get_episode_short_name(episode)}\n" - f"Updated language for all users" - ) - inline_message = message.replace("\n", " | ") - logger.info(f"Language update for new episode: {inline_message}") - if self.notifier is None: - return - self.notifier.notify(title, message) + title = f"PlexAutoLanguages - {track_changes.title}" + if isinstance(track_changes, TrackChanges): + self.notifier.notify_user(title, track_changes.description, track_changes.username, track_changes.event_type) + else: + self.notifier.notify(title, track_changes.description, track_changes.event_type) def start_deep_analysis(self): + # History min_date = datetime.now() - timedelta(days=1) history = self._plex.history(mindate=min_date) for episode in [media for media in history if isinstance(media, Episode)]: @@ -211,4 +199,4 @@ def start_deep_analysis(self): if user is None: continue episode.reload() - self.change_default_tracks_if_needed(user.name, episode) + self.change_tracks(user.name, episode, EventType.SCHEDULER) diff --git a/plex_auto_languages/track_changes.py b/plex_auto_languages/track_changes.py index ffa3921..23fc4f2 100644 --- a/plex_auto_languages/track_changes.py +++ b/plex_auto_languages/track_changes.py @@ -3,6 +3,7 @@ from plexapi.media import AudioStream, SubtitleStream, MediaPart from plex_auto_languages.utils.logger import get_logger +from plex_auto_languages.constants import EventType logger = get_logger() @@ -10,12 +11,23 @@ class TrackChanges(): - def __init__(self, username: str, reference: Episode): + def __init__(self, username: str, reference: Episode, event_type: EventType): self._reference = reference self._username = username + self._event_type = event_type self._audio_stream, self._subtitle_stream = self._get_selected_streams(reference) self._changes = [] self._description = "" + self._title = "" + self._computed = False + + @property + def computed(self): + return self._computed + + @property + def event_type(self): + return self._event_type @property def description(self): @@ -25,9 +37,13 @@ def description(self): def inline_description(self): return self._description.replace("\n", " | ") + @property + def title(self): + return self._title + @property def reference_name(self): - return f"'{self._reference.show().title}' (S{self._reference.seasonNumber:02}E{self._reference.episodeNumber:02})" + return f"{self._reference.show().title} (S{self._reference.seasonNumber:02}E{self._reference.episodeNumber:02})" @property def has_changes(self): @@ -53,6 +69,8 @@ def get_episodes_to_update(self, update_level: str, update_strategy: str): return episodes def compute(self, episodes: List[Episode]): + logger.debug(f"[Language Update] Checking language update for show " + f"{self._reference.show()} and user '{self._username}' based on episode {self._reference}") self._changes = [] for episode in episodes: episode.reload() @@ -71,8 +89,12 @@ def compute(self, episodes: List[Episode]): (current_subtitle_stream is None or matching_subtitle_stream.id != current_subtitle_stream.id): self._changes.append((episode, part, SubtitleStream.STREAMTYPE, matching_subtitle_stream)) self._update_description(episodes) + self._computed = True def apply(self): + if not self.has_changes: + logger.debug(f"[Language Update] No changes to perform for episode {self._reference} and user '{self.username}'") + return logger.debug(f"[Language Update] Performing {len(self._changes)} change(s) for show {self._reference.show()}") for episode, part, stream_type, new_stream in self._changes: stream_type_name = "audio" if stream_type == AudioStream.STREAMTYPE else "subtitle" @@ -90,6 +112,7 @@ def _is_episode_after(self, episode: Episode): def _update_description(self, episodes: List[Episode]): if len(episodes) == 0: + self._title = "" self._description = "" return season_numbers = [e.seasonNumber for e in episodes] @@ -101,6 +124,7 @@ def _update_description(self, episodes: List[Episode]): range_str = f"{from_str} - {to_str}" if from_str != to_str else from_str nb_updated = len({e.key for e, _, _, _ in self._changes}) nb_total = len(episodes) + self._title = self._reference.show().title self._description = ( f"Show: {self._reference.show().title}\n" f"User: {self._username}\n" @@ -160,3 +184,61 @@ def _get_selected_streams(episode: Union[Episode, MediaPart]): audio_stream = ([a for a in episode.audioStreams() if a.selected] + [None])[0] subtitle_stream = ([s for s in episode.subtitleStreams() if s.selected] + [None])[0] return audio_stream, subtitle_stream + + +class NewOrUpdatedTrackChanges(): + + def __init__(self, event_type: EventType): + self._episode = None + self._event_type = event_type + self._track_changes = [] + self._description = "" + self._title = "" + + @property + def episode_name(self): + if self._episode is None: + return "" + return f"{self._episode.show().title} (S{self._episode.seasonNumber:02}E{self._episode.episodeNumber:02})" + + @property + def event_type(self): + return self._event_type + + @property + def description(self): + return self._description + + @property + def inline_description(self): + return self._description.replace("\n", " | ") + + @property + def title(self): + return self._title + + @property + def has_changes(self): + return sum([1 for tc in self._track_changes if tc.has_changes]) > 0 + + def change_track_for_user(self, username: str, reference: Episode, episode: Episode): + self._episode = episode + track_changes = TrackChanges(username, reference, self._event_type) + track_changes.compute([episode]) + track_changes.apply() + self._track_changes.append(track_changes) + self._update_description() + + def _update_description(self): + if len(self._track_changes) == 0: + self._title = "" + self._description = "" + self._episode = None + return + event_str = "New" if self._event_type == EventType.NEW_EPISODE else "Updated" + self._title = f"{event_str}: {self.episode_name}" + self._description = ( + f"Episode: {self.episode_name}\n" + f"Status: {event_str} episode\n" + f"Updated for all users" + ) diff --git a/plex_auto_languages/utils/notifier.py b/plex_auto_languages/utils/notifier.py index e39ef9d..efa83b5 100644 --- a/plex_auto_languages/utils/notifier.py +++ b/plex_auto_languages/utils/notifier.py @@ -1,39 +1,74 @@ from typing import List, Union from apprise import Apprise +from plex_auto_languages.constants import EventType + class Notifier(): def __init__(self, configs: List[Union[str, dict]]): - self._global_apprise = Apprise() + self._global_apprise = ConditionalApprise() self._user_apprise = {} for config in configs: if isinstance(config, str): - self._add_urls([config], None) + self._add_urls([config]) if isinstance(config, dict) and "urls" in config: - targets = config.get("urls") - targets = [targets] if isinstance(targets, str) else targets + urls = config.get("urls") + urls = [urls] if isinstance(urls, str) else urls usernames = config.get("users", None) - usernames = [usernames] if isinstance(usernames, str) else usernames - self._add_urls(targets, usernames) + if usernames is None: + usernames = [] + elif isinstance(usernames, str): + usernames = [usernames] + event_types = config.get("events", None) + if event_types is None: + event_types = [] + elif isinstance(event_types, str): + event_types = [EventType[event_types.upper()]] + elif isinstance(event_types, list): + event_types = [EventType[et.upper()] for et in event_types] + self._add_urls(urls, usernames, event_types) - def _add_urls(self, urls: List[str], usernames: List[str] = None): + def _add_urls(self, urls: List[str], usernames: List[str] = None, event_types: List[EventType] = None): if usernames is None or len(usernames) == 0: for url in urls: self._global_apprise.add(url) + if event_types is not None: + self._global_apprise.add_event_types(event_types) return for username in usernames: - user_apprise = self._user_apprise.setdefault(username, Apprise()) + user_apprise = self._user_apprise.setdefault(username, ConditionalApprise()) for url in urls: user_apprise.add(url) + if event_types is not None: + user_apprise.add_event_types(event_types) - def notify(self, title: str, message: str): - self._global_apprise.notify(title=title, body=message) + def notify(self, title: str, message: str, event_type: EventType): + self._global_apprise.notifiy_if_needed(title, message, event_type) - def notify_user(self, title: str, message: str, username: str): - self._global_apprise.notify(title=title, body=message) + def notify_user(self, title: str, message: str, username: str, event_type: EventType): + self._global_apprise.notifiy_if_needed(title, message, event_type) if username is None or username not in self._user_apprise: return - for user_apprise in self._user_apprise[username]: - user_apprise.notify(title=title, body=message) + user_apprise = self._user_apprise[username] + user_apprise.notifiy_if_needed(title, message, event_type) + + +class ConditionalApprise(Apprise): + + def __init__(self): + super().__init__() + self._event_types = set() + + def add_event_type(self, event_type: EventType): + self._event_types.add(event_type) + + def add_event_types(self, event_types: List[EventType]): + for event_type in event_types: + self.add_event_type(event_type) + + def notifiy_if_needed(self, title: str, body: str, event_type: EventType): + if len(self._event_types) != 0 and event_type not in self._event_types: + return + self.notify(title=title, body=body)