diff --git a/README.md b/README.md index 703d0ea..a50bf98 100755 --- a/README.md +++ b/README.md @@ -98,7 +98,14 @@ You will need to set the following environment variables: | TC_TERMINATE_MESSAGE | No | Message sent to users when a stream is killed | "Your stream has ended." | | TC_SERVER_NAME | No | Name of the Plex server.
Will use provided; if not provided, will use "Plex"; if provided string is empty, will attempt to extract Plex Media Server name via Tautulli. | "Plex" | | TC_USE_24_HOUR_TIME | No | Whether to display times in 24-hour time | "False" | -| TC_ANON_USERS | No | Whether to hide usernames in the streams view | "False" | +| TC_HIDE_USERNAMES | No | Whether to hide usernames in the streams view | "False" | +| TC_HIDE_PLATFORMS | No | Whether to hide platforms in the streams view | "False" | +| TC_HIDE_PLAYER_NAMES | No | Whether to hide player names in the streams view | "False" | +| TC_HIDE_QUALITY | No | Whether to hide quality profiles in the streams view | "False" | +| TC_HIDE_BANDWIDTH | No | Whether to hide bandwidth in the streams view | "False" | +| TC_HIDE_TRANSCODE | No | Whether to hide transcoding statuses in the streams view | "False" | +| TC_HIDE_PROGRESS | No | Whether to hide stream progress in the streams view | "False" | +| TC_HIDE_ETA | No | Whether to hide stream ETAs in the streams view | "False" | | | | | | TC_VC_STATS_CATEGORY_NAME | No | Name of the stats voice channel category | "Tautulli Stats" | | TC_VC_STREAM_COUNT | No | Whether to display current stream count in a voice channel | "False" | | TC_VC_TRANSCODE_COUNT | No | Whether to display current transcode count in a voice channel | "False" | diff --git a/config.yaml.example b/config.yaml.example index 04d8509..3e7634d 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -36,6 +36,15 @@ Tautulli: TVEpisodeCount: true MusicArtistCount: true MusicTrackCount: true + Anonymize: + HideUsernames: false + HidePlayerNames: false + HidePlatforms: false + HideQuality: false + HideBandwidth: false + HideTranscode: false + HideProgress: false + HideETA: false Discord: Connection: diff --git a/modules/config_parser.py b/modules/config_parser.py index 850b186..58f1c15 100644 --- a/modules/config_parser.py +++ b/modules/config_parser.py @@ -1,12 +1,13 @@ import json import os -from typing import List +from typing import List, Dict, Any import confuse import yaml -from modules import statics import modules.logs as logging +from modules import statics +from modules.text_manager import TextManager def _extract_bool(value): @@ -90,7 +91,7 @@ def refresh_interval(self) -> int: def server_name(self) -> str: return self._customization._get_value(key='ServerName', default="Plex", env_name_override="TC_SERVER_NAME") - + @property def anonymous_users(self) -> bool: value = self._customization._get_value(key='AnonymousUsers', default=False, @@ -213,7 +214,7 @@ def show_music_track_count(self) -> bool: return _extract_bool(value) @property - def voice_channel_settings(self): + def voice_channel_settings(self) -> Dict[str, Any]: return { statics.KEY_STATS_CATEGORY_NAME: self.stats_voice_channel_category_name, statics.KEY_COUNT: self.display_stream_count, @@ -235,7 +236,7 @@ def voice_channel_settings(self): @property def any_live_stats_channels_enabled(self) -> bool: keys = [statics.KEY_COUNT, statics.KEY_TRANSCODE_COUNT, statics.KEY_BANDWIDTH, - statics.KEY_LAN_BANDWIDTH, statics.KEY_REMOTE_BANDWIDTH, statics.KEY_PLEX_STATUS] + statics.KEY_LAN_BANDWIDTH, statics.KEY_REMOTE_BANDWIDTH, statics.KEY_PLEX_STATUS] return any([self.voice_channel_settings.get(key, False) for key in keys]) @property @@ -243,6 +244,71 @@ def any_library_stats_channels_enabled(self) -> bool: keys = [statics.KEY_STATS] return any([self.voice_channel_settings.get(key, False) for key in keys]) + @property + def _anonymize_rules(self) -> ConfigSection: + return self._customization._get_subsection(key="Anonymize") + + @property + def _anonymize_hide_usernames(self) -> bool: + value = self._anonymize_rules._get_value(key="HideUsernames", default=False, + env_name_override="TC_HIDE_USERNAMES") + return _extract_bool(value) + + @property + def _anonymize_hide_platforms(self) -> bool: + value = self._anonymize_rules._get_value(key="HidePlatforms", default=False, + env_name_override="TC_HIDE_PLATFORMS") + return _extract_bool(value) + + @property + def _anonymize_hide_player_names(self) -> str: + return self._anonymize_rules._get_value(key="HidePlayerNames", default=False, + env_name_override="TC_HIDE_PLAYER_NAMES") + + @property + def _anonymize_hide_quality(self) -> bool: + value = self._anonymize_rules._get_value(key="HideQuality", default=False, + env_name_override="TC_HIDE_QUALITY") + return _extract_bool(value) + + @property + def _anonymize_hide_bandwidth(self) -> bool: + value = self._anonymize_rules._get_value(key="HideBandwidth", default=False, + env_name_override="TC_HIDE_BANDWIDTH") + return _extract_bool(value) + + @property + def _anonymize_hide_transcode_decision(self) -> bool: + value = self._anonymize_rules._get_value(key="HideTranscode", default=False, + env_name_override="TC_HIDE_TRANSCODE") + return _extract_bool(value) + + @property + def _anonymize_hide_progress(self) -> bool: + value = self._anonymize_rules._get_value(key="HideProgress", default=False, + env_name_override="TC_HIDE_PROGRESS") + return _extract_bool(value) + + @property + def _anonymize_hide_eta(self) -> bool: + value = self._anonymize_rules._get_value(key="HideETA", default=False, + env_name_override="TC_HIDE_ETA") + return _extract_bool(value) + + @property + def text_manager(self) -> TextManager: + anonymous_rules = { + statics.KEY_HIDE_USERNAMES: self._anonymize_hide_usernames, + statics.KEY_HIDE_PLAYER_NAMES: self._anonymize_hide_player_names, + statics.KEY_HIDE_PLATFORMS: self._anonymize_hide_platforms, + statics.KEY_HIDE_QUALITY: self._anonymize_hide_quality, + statics.KEY_HIDE_BANDWIDTH: self._anonymize_hide_bandwidth, + statics.KEY_HIDE_TRANSCODING: self._anonymize_hide_transcode_decision, + statics.KEY_HIDE_PROGRESS: self._anonymize_hide_progress, + statics.KEY_HIDE_ETA: self._anonymize_hide_eta, + } + return TextManager(anon_rules=anonymous_rules) + class DiscordConfig(ConfigSection): def __init__(self, data, pull_from_env: bool = True): @@ -270,7 +336,8 @@ def admin_ids(self) -> List[str]: @property def channel_name(self) -> str: - return self._connection._get_value(key="ChannelName", default="tauticord", env_name_override="TC_DISCORD_CHANNEL_NAME") + return self._connection._get_value(key="ChannelName", default="tauticord", + env_name_override="TC_DISCORD_CHANNEL_NAME") @property def _customization(self) -> ConfigSection: diff --git a/modules/statics.py b/modules/statics.py index 281b550..4ab680d 100644 --- a/modules/statics.py +++ b/modules/statics.py @@ -4,17 +4,6 @@ STANDARD_EMOJIS_FOLDER = "resources/emojis/standard" NITRO_EMOJIS_FOLDER = "resources/emojis/nitro" -sessions_message = """{stream_count} {word}""" -transcodes_message = """{transcode_count} {word}""" -bandwidth_message = """{emoji} {bandwidth}""" -lan_bandwidth_message = """({emoji} {bandwidth})""" - -session_title_message = """{emoji} | {icon} {media_type_icon} *{title}*""" -session_user_message = """{emoji} **{username}**""" -session_player_message = """{emoji} **{product}** ({player})""" -session_details_message = """{emoji} **{quality_profile}** ({bandwidth}){transcoding}""" -session_progress_message = """{emoji} **{progress}** (ETA: {eta})""" - voice_channel_order = { 'count': 1, 'transcodes': 2, @@ -40,4 +29,16 @@ KEY_SHOW_MUSIC_ARTISTS = "show_music_artists" KEY_SHOW_MUSIC_TRACKS = "show_music_tracks" +KEY_ANONYMOUS_SETTINGS = "anonymous_settings" +KEY_HIDE_USERNAMES = "hide_usernames" +KEY_HIDE_PLATFORMS = "hide_platforms" +KEY_HIDE_PLAYER_NAMES = "anonymize_players" +KEY_HIDE_QUALITY = "hide_quality" +KEY_HIDE_BANDWIDTH = "hide_bandwidth" +KEY_HIDE_TRANSCODING = "hide_transcoding" +KEY_HIDE_PROGRESS = "hide_progress" +KEY_HIDE_ETA = "hide_eta" +KEY_DISABLE_TERMINATION = "hide_termination" + + MAX_STREAM_COUNT = 36 diff --git a/modules/tautulli_connector.py b/modules/tautulli_connector.py index 8334f56..8818df6 100644 --- a/modules/tautulli_connector.py +++ b/modules/tautulli_connector.py @@ -4,10 +4,10 @@ import tautulli import modules.logs as logging -import modules.statics as statics from modules import utils from modules.emojis import EmojiManager from modules.settings_transports import LibraryVoiceChannelsVisibilities +from modules.text_manager import TextManager session_ids = {} @@ -126,32 +126,6 @@ def transcoding_stub(self) -> str: def stream_container_decision(self) -> str: return self._data['stream_container_decision'] - def get_session_title(self, session_number: int, emoji_manager: EmojiManager) -> str: - emoji = emoji_manager.emoji_from_stream_number(number=session_number) - return statics.session_title_message.format(emoji=emoji, - icon=self.get_status_icon(emoji_manager=emoji_manager), - media_type_icon=self.get_type_icon(emoji_manager=emoji_manager), - title=self.title) - - def get_session_user(self, emoji_manager: EmojiManager, anon_users: bool) -> str: - emoji = emoji_manager.get_emoji(key="person") - return statics.session_user_message.format(emoji=emoji, username="Anonymous" if anon_users else self.username) - - def get_session_player(self, emoji_manager: EmojiManager, anon_users: bool) -> str: - emoji = emoji_manager.get_emoji(key="device") - return statics.session_player_message.format(emoji=emoji, product=self.product, player="Anonymous" if anon_users else self.player) - - def get_session_details(self, emoji_manager: EmojiManager) -> str: - emoji = emoji_manager.get_emoji(key="resolution") - return statics.session_details_message.format(emoji=emoji, quality_profile=self.quality_profile, - bandwidth=self.bandwidth, - transcoding=self.transcoding_stub) - - def get_session_progress(self, emoji_manager: EmojiManager) -> str: - emoji = emoji_manager.get_emoji(key="progress") - return statics.session_progress_message.format(emoji=emoji, progress=self.progress_marker, eta=self.eta) - - class Activity: def __init__(self, activity_data, time_settings: dict, emoji_manager: EmojiManager): self._data = activity_data @@ -200,24 +174,6 @@ def wan_bandwidth(self) -> Union[str, None]: except: return None - def get_message(self) -> str: - overview_message = "" - if self.stream_count > 0: - overview_message += statics.sessions_message.format(stream_count=self.stream_count, - word=utils.make_plural(word='stream', - count=self.stream_count)) - if self.transcode_count > 0: - overview_message += f" ({statics.transcodes_message.format(transcode_count=self.transcode_count, word=utils.make_plural(word='transcode', count=self.transcode_count))})" - - if self.total_bandwidth: - bandwidth_emoji = self._emoji_manager.get_emoji(key='bandwidth') - overview_message += f" @ {statics.bandwidth_message.format(emoji=bandwidth_emoji, bandwidth=self.total_bandwidth)}" - if self.lan_bandwidth: - lan_bandwidth_emoji = self._emoji_manager.get_emoji(key='home') - overview_message += f" {statics.lan_bandwidth_message.format(emoji=lan_bandwidth_emoji, bandwidth=self.lan_bandwidth)}" - - return overview_message - @property def sessions(self) -> List[Session]: return [Session(session_data=session_data, time_settings=self._time_settings) for session_data in @@ -229,27 +185,15 @@ def __init__(self, session: Session, session_number: int): self._session = session self._session_number = session_number - def get_title(self, emoji_manager: EmojiManager) -> str: + def get_title(self, emoji_manager: EmojiManager, text_manager: TextManager) -> str: try: - return self._session.get_session_title(session_number=self._session_number, emoji_manager=emoji_manager) + return text_manager.session_title(session=self._session, session_number=self._session_number, emoji_manager=emoji_manager) except Exception as title_exception: return "Unknown" - def get_player(self, emoji_manager: EmojiManager, anon_users: bool) -> str: - return self._session.get_session_player(emoji_manager=emoji_manager, anon_users=anon_users) - - def get_user(self, emoji_manager: EmojiManager, anon_users: bool) -> str: - return self._session.get_session_user(emoji_manager=emoji_manager, anon_users=anon_users) - - def get_details(self, emoji_manager: EmojiManager) -> str: - return self._session.get_session_details(emoji_manager=emoji_manager) - - def get_progress(self, emoji_manager: EmojiManager) -> str: - return self._session.get_session_progress(emoji_manager=emoji_manager) - - def get_body(self, emoji_manager: EmojiManager, anon_users: bool) -> str: + def get_body(self, emoji_manager: EmojiManager, text_manager: TextManager) -> str: try: - return f"{self.get_user(emoji_manager=emoji_manager, anon_users=anon_users)}\n{self.get_player(emoji_manager=emoji_manager, anon_users=anon_users)}\n{self.get_details(emoji_manager=emoji_manager)}\n{self.get_progress(emoji_manager=emoji_manager)}" + return text_manager.session_body(session=self._session, emoji_manager=emoji_manager) except Exception as body_exception: logging.error(str(body_exception)) return f"Could not display data for session {self._session_number}" @@ -257,20 +201,20 @@ def get_body(self, emoji_manager: EmojiManager, anon_users: bool) -> str: class TautulliDataResponse: def __init__(self, - overview_message: str, + activity: Union[Activity, None], server_name: str, emoji_manager: EmojiManager, + text_manager: TextManager, streams_info: List[TautulliStreamInfo] = None, plex_pass: bool = False, - error_occurred: bool = False, - anon_users: bool = False): - self._overview_message = overview_message + error_occurred: bool = False): + self._activity = activity self._streams = streams_info or [] self.plex_pass = plex_pass self.error = error_occurred self._emoji_manager = emoji_manager self._server_name = server_name - self.anon_users = anon_users + self._text_manager = text_manager @property def embed(self) -> discord.Embed: @@ -278,11 +222,13 @@ def embed(self) -> discord.Embed: return discord.Embed(title="No current activity") embed = discord.Embed(title=f"Current activity on {self._server_name}") for stream in self._streams: - embed.add_field(name=stream.get_title(emoji_manager=self._emoji_manager), - value=stream.get_body(emoji_manager=self._emoji_manager, anon_users=self.anon_users), inline=False) - footer_text = self._overview_message - if self.plex_pass: - footer_text += f"\n\nTo terminate a stream, react with the stream number." + embed.add_field(name=stream.get_title(emoji_manager=self._emoji_manager, text_manager=self._text_manager), + value=stream.get_body(emoji_manager=self._emoji_manager, text_manager=self._text_manager), + inline=False) + footer_text = self._text_manager.overview_footer(no_connection=self.error, + activity=self._activity, + emoji_manager=self._emoji_manager, + add_termination_tip=self.plex_pass) embed.set_footer(text=footer_text) return embed @@ -295,11 +241,11 @@ def __init__(self, analytics, plex_pass: bool, time_settings: dict, + text_manager: TextManager, server_name: str = None, - anon_users: bool = False, ): self.base_url = base_url - self.anon_users = anon_users + self.text_manager = text_manager self.api_key = api_key try: self.api = tautulli.RawAPI(base_url=base_url, api_key=api_key) @@ -343,15 +289,17 @@ def refresh_data(self, emoji_manager: EmojiManager) -> Tuple[ self._error_and_analytics(error_message=err, function_name='refresh_data (ValueError)') pass logging.debug(f"Count: {count}") - return TautulliDataResponse(overview_message=activity.get_message(), + return TautulliDataResponse(activity=activity, emoji_manager=emoji_manager, + text_manager=self.text_manager, streams_info=session_details, plex_pass=self.plex_pass, - server_name=self.server_name, - anon_users=self.anon_users), count, activity, self.is_plex_server_online() + server_name=self.server_name), count, activity, self.is_plex_server_online() except KeyError as e: self._error_and_analytics(error_message=e, function_name='refresh_data (KeyError)') - return TautulliDataResponse(overview_message="**Connection lost.**", emoji_manager=emoji_manager, + return TautulliDataResponse(activity=None, + emoji_manager=emoji_manager, + text_manager=self.text_manager, error_occurred=True, server_name=self.server_name), 0, None, False # If Tautulli is offline, assume Plex is offline diff --git a/modules/text_manager.py b/modules/text_manager.py new file mode 100644 index 0000000..1b1bf17 --- /dev/null +++ b/modules/text_manager.py @@ -0,0 +1,127 @@ +from typing import Dict, Any, Union + +from modules import statics, utils +from modules.emojis import EmojiManager + + +class TextManager: + """ + Manages text formatting and anonymization. + """ + def __init__(self, anon_rules: Dict[str, Any]) -> None: + self._anon_rules: dict = anon_rules + self._anon_hide_usernames: bool = anon_rules.get(statics.KEY_HIDE_USERNAMES, False) + self._anon_hide_player_names: bool = anon_rules.get(statics.KEY_HIDE_PLAYER_NAMES, False) + self._anon_hide_platforms: bool = anon_rules.get(statics.KEY_HIDE_PLATFORMS, False) + self._anon_hide_quality: bool = anon_rules.get(statics.KEY_HIDE_QUALITY, False) + self._anon_hide_bandwidth: bool = anon_rules.get(statics.KEY_HIDE_BANDWIDTH, False) + self._anon_hide_transcoding: bool = anon_rules.get(statics.KEY_HIDE_TRANSCODING, False) + self._anon_hide_progress: bool = anon_rules.get(statics.KEY_HIDE_PROGRESS, False) + self._anon_hide_eta: bool = anon_rules.get(statics.KEY_HIDE_ETA, False) + + def _session_user_message(self, session, emoji_manager: EmojiManager) -> Union[str, None]: + if self._anon_hide_usernames: + return None + + emoji = emoji_manager.get_emoji(key="person") + username = session.username + stub = f"""{emoji} **{username}**""" + + return stub + + def _session_player_message(self, session, emoji_manager: EmojiManager) -> Union[str, None]: + if self._anon_hide_platforms and self._anon_hide_player_names: + return None + + emoji = emoji_manager.get_emoji(key="device") + player = None if self._anon_hide_player_names else session.player + product = None if self._anon_hide_platforms else session.product + + stub = f"""{emoji}""" + if player is not None: + stub += f""" **{player}**""" + # Only optionally show product if player is shown. + if product is not None: + stub += f""" ({product})""" + + return stub + + def _session_details_message(self, session, emoji_manager: EmojiManager) -> Union[str, None]: + if self._anon_hide_quality and self._anon_hide_bandwidth and self._anon_hide_transcoding: + return None + + quality_profile = None if self._anon_hide_quality else session.quality_profile + bandwidth = None if self._anon_hide_bandwidth else session.bandwidth + transcoding = None if self._anon_hide_transcoding else session.transcoding_stub + + emoji = emoji_manager.get_emoji(key="resolution") + stub = f"""{emoji}""" + if quality_profile is not None: + stub += f""" **{quality_profile}**""" + # Only optionally show bandwidth if quality profile is shown. + if bandwidth is not None: + stub += f""" ({bandwidth})""" + if transcoding is not None: + stub += f"""{transcoding}""" + + return stub + + def _session_progress_message(self, session, emoji_manager: EmojiManager) -> Union[str, None]: + if self._anon_hide_progress: + return None + + emoji = emoji_manager.get_emoji(key="progress") + progress = session.progress_marker + stub = f"""{emoji} **{progress}**""" + if not self._anon_hide_eta: + eta = session.eta + stub += f""" (ETA: {eta})""" + + return stub + + def session_title(self, session, session_number: int, emoji_manager: EmojiManager) -> str: + emoji = emoji_manager.emoji_from_stream_number(number=session_number) + icon = session.get_status_icon(emoji_manager=emoji_manager) + media_type_icon = session.get_type_icon(emoji_manager=emoji_manager) + title = session.title + return f"""{emoji} | {icon} {media_type_icon} *{title}*""" + + def session_body(self, session, emoji_manager: EmojiManager) -> str: + user_message = self._session_user_message(session=session, emoji_manager=emoji_manager) + player_message = self._session_player_message(session=session, emoji_manager=emoji_manager) + details_message = self._session_details_message(session=session, emoji_manager=emoji_manager) + progress_message = self._session_progress_message(session=session, emoji_manager=emoji_manager) + + stubs = [user_message, player_message, details_message, progress_message] + stubs = [stub for stub in stubs if stub is not None] + return "\n".join(stubs) + + def overview_footer(self, no_connection: bool, activity, emoji_manager: EmojiManager, add_termination_tip: bool) -> str: + if no_connection or activity is None: + return "**Connection lost.**" + + if activity.stream_count == 0: + return "**No active sessions.**" + + stream_count = activity.stream_count + stream_count_word = utils.make_plural(word='stream', count=stream_count) + overview_message = f"""{stream_count} {stream_count_word}""" + + if activity.transcode_count > 0 and not self._anon_hide_transcoding: + transcode_count = activity.transcode_count + transcode_count_word = utils.make_plural(word='transcode', count=transcode_count) + overview_message += f""" ({transcode_count} {transcode_count_word})""" + + if activity.total_bandwidth and not self._anon_hide_bandwidth: + bandwidth_emoji = emoji_manager.get_emoji(key='bandwidth') + bandwidth = activity.total_bandwidth + overview_message += f""" @ {bandwidth_emoji} {bandwidth}""" + if activity.lan_bandwidth: + lan_bandwidth_emoji = emoji_manager.get_emoji(key='home') + lan_bandwidth = activity.lan_bandwidth + overview_message += f""" {lan_bandwidth_emoji} {lan_bandwidth}""" + + if add_termination_tip: + overview_message += f"\n\nTo terminate a stream, react with the stream number." + + return overview_message diff --git a/run.py b/run.py index 7996e79..e7cd149 100755 --- a/run.py +++ b/run.py @@ -59,7 +59,7 @@ plex_pass=config.tautulli.has_plex_pass, time_settings=config.tautulli.time_settings, server_name=config.tautulli.server_name, - anon_users=config.tautulli.anonymous_users, + text_manager=config.tautulli.text_manager, ) discord_connector = discord.DiscordConnector( diff --git a/templates/tauticord.xml b/templates/tauticord.xml index 480fd6f..a60c6ba 100644 --- a/templates/tauticord.xml +++ b/templates/tauticord.xml @@ -29,7 +29,14 @@ Plex Your stream has ended. False - False + False + False + False + False + False + False + False + False Tautulli Stats False False