Skip to content

Commit

Permalink
Improve notifications (#29)
Browse files Browse the repository at this point in the history
* Add per-user notification setup

* Add documentation

* Add NotificationBuilder

* Reduce notification count for newly added episodes
  • Loading branch information
RemiRigal committed Apr 30, 2022
1 parent fc5b180 commit acfa3ad
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 32 deletions.
12 changes: 11 additions & 1 deletion README.md
Expand Up @@ -130,8 +130,18 @@ plexautolanguages:
enable: true
# An array of Apprise configurations, see Apprise docs for more information: https://github.com/caronc/apprise
apprise_configs:
# This URL will be notified of all changes
- "discord://webhook_id/webhook_token"
- "gotify://hostname/token"
# These URLs will only be notified of language change for users "MyUser1" and "MyUser2"
- urls:
- "gotify://hostname/token"
- "pover://user@token"
users:
- "MyUser1"
- "MyUser2"
# This URL will only be notified of language change for user "MyUser3"
- urls: "tgram://bottoken/ChatID"
users: "MyUser3"
- "..."

# Whether or not to enable the debug mode, defaults to 'false'
Expand Down
70 changes: 39 additions & 31 deletions main.py
Expand Up @@ -5,7 +5,6 @@
from time import sleep
from typing import List, Union
from datetime import datetime, timedelta
from apprise import Apprise
from plexapi.video import Episode
from plexapi.server import PlexServer
from plexapi.media import AudioStream, SubtitleStream
Expand All @@ -16,13 +15,14 @@
from utils.scheduler import Scheduler
from utils.configuration import Configuration
from utils.healthcheck import HealthcheckServer
from utils.notifier import Notifier, NotificationBuilder


class PlexAutoLanguages(object):
class PlexAutoLanguages():

def __init__(self, user_config_path: str):
self.alive = False
self.notifier = None
self.plex_alert_listener = None
self.set_signal_handlers()
self.healthcheck_server = HealthcheckServer("Plex-Auto-Languages", self.is_ready, self.is_healthy)
self.healthcheck_server.start()
Expand All @@ -43,11 +43,9 @@ def __init__(self, user_config_path: str):
if self.config.get("scheduler.enable"):
self.scheduler = Scheduler(self.config.get("scheduler.schedule_time"), self.scheduler_callback)
# Notifications
self.apprise = None
self.notifier = None
if self.config.get("notifications.enable"):
self.apprise = Apprise()
for apprise_config in self.config.get("notifications.apprise_configs"):
self.apprise.add(apprise_config)
self.notifier = Notifier(self.config.get("notifications.apprise_configs"))

def get_plex_user_id(self):
plex_username = self.plex.myPlexAccount().username
Expand All @@ -62,7 +60,7 @@ def is_ready(self):
return self.alive

def is_healthy(self):
return self.alive and self.notifier is not None and self.notifier.is_alive()
return self.alive and self.plex_alert_listener is not None and self.plex_alert_listener.is_alive()

def set_signal_handlers(self):
signal.signal(signal.SIGINT, self.stop)
Expand All @@ -74,12 +72,12 @@ def stop(self, *args):

def start(self):
logger.info("Starting alert listener")
self.notifier = self.plex.startAlertListener(self.alert_listener_callback)
self.plex_alert_listener = self.plex.startAlertListener(self.alert_listener_callback)
if self.scheduler:
logger.info("Starting scheduler")
self.scheduler.start()
self.alive = True
while self.notifier.is_alive() and self.alive:
while self.plex_alert_listener.is_alive() and self.alive:
sleep(1)
if self.scheduler:
logger.info("Stopping scheduler")
Expand Down Expand Up @@ -235,7 +233,7 @@ def process_status_message(self, message: dict):
def process_status(self, status: dict):
if status.get("title", None) != "Library scan complete":
return
logger.debug("[Status] Library scan complete")
logger.info("[Status] Library scan complete")
for section in [s for s in self.plex.library.sections() if isinstance(s, ShowSection)]:
recent = section.searchEpisodes(filters={"addedAt>>": "5m"})
if len(recent) == 0:
Expand Down Expand Up @@ -273,30 +271,37 @@ def process_new_episode(self, item_id: Union[int, str]):
user = PlexUtils.get_user_from_user_id(self.plex, user_id)
if user is None:
return
self.change_default_tracks_if_needed(user.name, reference, episodes=[user_item])
self.change_default_tracks_if_needed(user.name, reference, episodes=[user_item], notify=False)
self.notify_new_episode(PlexUtils.fetch_item(self.plex, item_id))

def notify_changes(self, username: str, episode: Episode, episodes: List[Episode], nb_updated: int, nb_total: int):
target_audio, target_subtitles = PlexUtils.get_selected_streams(episode)
season_numbers = [e.seasonNumber for e in episodes]
min_season_number, max_season_number = min(season_numbers), max(season_numbers)
min_episode_number = min([e.episodeNumber for e in episodes if e.seasonNumber == min_season_number])
max_episode_number = max([e.episodeNumber for e in episodes if e.seasonNumber == max_season_number])
from_str = f"S{min_season_number:02}E{min_episode_number:02}"
to_str = f"S{max_season_number:02}E{max_episode_number:02}"
range_str = f"{from_str} - {to_str}" if from_str != to_str else from_str
title = f"PlexAutoLanguages - {episode.show().title}"
builder = NotificationBuilder()
builder.username(username)
builder.audio_stream(target_audio)
builder.subtitle_stream(target_subtitles)
builder.episode(episode)
builder.episodes(episodes)
builder.nb_updated(nb_updated)
builder.nb_total(nb_total)
_, inline_message = builder.build(True)
logger.info(f"Language update: {inline_message}")
if self.notifier is None:
return
title, message = builder.build(False)
self.notifier.notify(username, title, message)

def notify_new_episode(self, episode: Episode):
title = "PlexAutoLanguages - New episode"
message = (
f"Show: {episode.show().title}\n"
f"User: {username}\n"
f"Audio: {target_audio.displayTitle if target_audio is not None else 'None'}\n"
f"Subtitles: {target_subtitles.displayTitle if target_subtitles is not None else 'None'}\n"
f"Updated episodes: {nb_updated}/{nb_total} ({range_str})"
f"Episode: {PlexUtils.get_episode_short_name(episode)}\n"
f"Updated language for all users"
)
inline_message = message.replace("\n", " | ")
logger.info(f"Language update: {inline_message}")
if self.apprise is None:
logger.info(f"Language update for new episode: {inline_message}")
if self.notifier is None:
return
self.apprise.notify(title=title, body=message)
self.notifier.notify(None, title, message)

def scheduler_callback(self):
logger.info("Starting scheduler task")
Expand All @@ -309,7 +314,8 @@ def scheduler_callback(self):
episode.reload()
self.change_default_tracks_if_needed(user.name, episode)

def change_default_tracks_if_needed(self, username: str, episode: Episode, episodes: List[Episode] = None):
def change_default_tracks_if_needed(self, username: str, episode: Episode, episodes: List[Episode] = None,
notify: bool = True):
logger.debug(f"[Language Update] "
f"Checking language update for show {episode.show()} and user '{username}' based on episode {episode}")
if episodes is None:
Expand All @@ -322,7 +328,7 @@ def change_default_tracks_if_needed(self, username: str, episode: Episode, episo
changes = PlexUtils.get_track_changes(episode, episodes)
if len(changes) == 0:
logger.debug(f"[Language Update] No changes to perform for show {episode.show()} and user '{username}'")
return
return False

# Perform changes
logger.debug(f"[Language Update] Performing {len(changes)} change(s) for show {episode.show()}")
Expand All @@ -339,7 +345,9 @@ def change_default_tracks_if_needed(self, username: str, episode: Episode, episo
# Notify changes
nb_updated_episodes = len({e.key for e, _, _, _ in changes})
nb_total_episodes = len(episodes)
self.notify_changes(username, episode, episodes, nb_updated_episodes, nb_total_episodes)
if notify:
self.notify_changes(username, episode, episodes, nb_updated_episodes, nb_total_episodes)
return True


if __name__ == "__main__":
Expand Down
96 changes: 96 additions & 0 deletions utils/notifier.py
@@ -0,0 +1,96 @@
from typing import List, Union
from apprise import Apprise
from plexapi.video import Episode
from plexapi.media import AudioStream, SubtitleStream

from utils.logger import get_logger


logger = get_logger()


class Notifier(object):

def __init__(self, configs: List[Union[str, dict]]):
self._global_apprise = Apprise()
self._user_apprise = {}

for config in configs:
if isinstance(config, str):
self._add_urls([config], None)
if isinstance(config, dict) and "urls" in config:
targets = config.get("urls")
targets = [targets] if isinstance(targets, str) else targets
usernames = config.get("users", None)
usernames = [usernames] if isinstance(usernames, str) else usernames
self._add_urls(targets, usernames)

def _add_urls(self, urls: List[str], usernames: List[str] = None):
if usernames is None or len(usernames) == 0:
for url in urls:
self._global_apprise.add(url)
return
for username in usernames:
user_apprise = self._user_apprise.setdefault(username, Apprise())
for url in urls:
user_apprise.add(url)

def notify(self, username: str, title: str, message: str):
self._global_apprise.notify(title=title, body=message)
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)


class NotificationBuilder(object):

def __init__(self):
self._message = ""
self._username = None
self._audio_stream = None
self._subtitle_stream = None
self._episode = None
self._episodes = None
self._nb_updated = None
self._nb_total = None

def build(self, inline: bool = False):
separator = " | " if inline else "\n"
season_numbers = [e.seasonNumber for e in self._episodes]
min_season_number, max_season_number = min(season_numbers), max(season_numbers)
min_episode_number = min([e.episodeNumber for e in self._episodes if e.seasonNumber == min_season_number])
max_episode_number = max([e.episodeNumber for e in self._episodes if e.seasonNumber == max_season_number])
from_str = f"S{min_season_number:02}E{min_episode_number:02}"
to_str = f"S{max_season_number:02}E{max_episode_number:02}"
range_str = f"{from_str} - {to_str}" if from_str != to_str else from_str
title = f"PlexAutoLanguages - {self._episode.show().title}"
message = (
f"Show: {self._episode.show().title}{separator}"
f"User: {self._username}{separator}"
f"Audio: {self._audio_stream.displayTitle if self._audio_stream is not None else 'None'}{separator}"
f"Subtitles: {self._subtitle_stream.displayTitle if self._subtitle_stream is not None else 'None'}{separator}"
f"Updated episodes: {self._nb_updated}/{self._nb_total} ({range_str})"
)
return title, message

def username(self, username: str):
self._username = username

def audio_stream(self, audio_stream: AudioStream):
self._audio_stream = audio_stream

def subtitle_stream(self, subtitle_stream: SubtitleStream):
self._subtitle_stream = subtitle_stream

def episode(self, episode: Episode):
self._episode = episode

def episodes(self, episodes: List[Episode]):
self._episodes = episodes

def nb_updated(self, nb_updated: int):
self._nb_updated = nb_updated

def nb_total(self, nb_total: int):
self._nb_total = nb_total

0 comments on commit acfa3ad

Please sign in to comment.