From 62a472a8e13a96f4e32e6856b7e18a189134461b Mon Sep 17 00:00:00 2001 From: lasers Date: Tue, 2 Apr 2024 05:30:20 -0500 Subject: [PATCH 1/3] py3: add new helper: get_replacements_list --- py3status/py3.py | 68 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/py3status/py3.py b/py3status/py3.py index 8550f89f1b..00b893dfad 100644 --- a/py3status/py3.py +++ b/py3status/py3.py @@ -1,4 +1,5 @@ import os +import re import shlex import sys import time @@ -102,6 +103,7 @@ def __init__(self, module=None): self._format_placeholders = {} self._format_placeholders_cache = {} self._module = module + self._replacements = None self._report_exception_cache = set() self._thresholds = None self._threshold_gradients = {} @@ -177,7 +179,7 @@ def _thresholds_init(self): except TypeError: pass self._thresholds[None] = [(x[0], self._get_color(x[1])) for x in thresholds] - return + elif isinstance(thresholds, dict): for key, value in thresholds.items(): if isinstance(value, list): @@ -187,6 +189,25 @@ def _thresholds_init(self): pass self._thresholds[key] = [(x[0], self._get_color(x[1])) for x in value] + def _replacements_init(self): + """ + Initiate and check any replacements set + """ + replacements = getattr(self._py3status_module, "replacements", []) + self._replacements = {} + + if isinstance(replacements, list): + self._replacements[None] = [ + (re.compile(x[0], re.IGNORECASE), x[1]) for x in replacements + ] + + elif isinstance(replacements, dict): + for key, value in replacements.items(): + if isinstance(value, list): + self._replacements[key] = [ + (re.compile(x[0], re.IGNORECASE), x[1]) for x in value + ] + def _get_module_info(self, module_name): """ THIS IS PRIVATE AND UNSUPPORTED. @@ -710,6 +731,28 @@ def get_color_names_list(self, format_string, matches=None): found.add(name) return list(found) + def get_replacements_list(self, format_string): + """ + If possible, returns a list of filtered placeholders in ``format_string``. + """ + replacements = getattr(self._py3status_module, "replacements", None) + if not replacements or not format_string: + return [] + + if format_string not in self._format_placeholders: + placeholders = self._formatter.get_placeholders(format_string) + self._format_placeholders[format_string] = placeholders + else: + placeholders = self._format_placeholders[format_string] + + # filter placeholders + found = set() + for replacement in replacements: + for placeholder in placeholders: + if placeholder == replacement: + found.add(placeholder) + return list(found or placeholders) + def get_placeholders_list(self, format_string, matches=None): """ Returns a list of placeholders in ``format_string``. @@ -1214,6 +1257,29 @@ def threshold_get_color(self, value, name=None): return color + def replace(self, value, name=None): + """ + Replace string using replacements. + + :param value: string value to be replaced + :param name: accepts a name + """ + # If first run, then process the replacements data. + if self._replacements is None: + self._replacements_init() + + if not value or not isinstance(value, str): + return value + + name_used = name + if name_used not in self._replacements: + name_used = None + + for pattern, replacement in self._replacements.get(name_used, []): + value = re.sub(pattern, replacement, value) + + return value + def request( self, url, From bd2c92e0514bb6db55efefca7528d51703887817 Mon Sep 17 00:00:00 2001 From: lasers Date: Wed, 27 Mar 2024 00:44:39 -0500 Subject: [PATCH 2/3] playerctl, spotify: add self.py3.replace helper --- py3status/modules/playerctl.py | 10 +++- py3status/modules/spotify.py | 91 +++++++++------------------------- 2 files changed, 33 insertions(+), 68 deletions(-) diff --git a/py3status/modules/playerctl.py b/py3status/modules/playerctl.py index 782cdd303e..437b4f6865 100644 --- a/py3status/modules/playerctl.py +++ b/py3status/modules/playerctl.py @@ -1,7 +1,7 @@ r""" Display song/video and control players supported by playerctl -Playerctl is a command-line utility for controlling media players +Playerctl is a command-line utility for controlling media players that implement the MPRIS D-Bus Interface Specification. With Playerctl you can bind player actions to keys and get metadata about the currently playing song or video. @@ -25,6 +25,7 @@ '[\?if=status=Stopped .. ][[{artist}][\?soft - ][{title}|{player}]]]')* format_player_separator: show separator if more than one player (default ' ') players: list of players to track. An empty list tracks all players (default []) + replacements: specify a list/dict of string placeholders to modify (default None) seek_delta: time (in seconds) to change the playback's position by (default 5) thresholds: specify color thresholds to use for different placeholders (default {"status": [("Playing", "good"), ("Paused", "degraded"), ("Stopped", "bad")]}) @@ -100,6 +101,7 @@ class Py3status: ) format_player_separator = " " players = [] + replacements = None seek_delta = 5 thresholds = {"status": [("Playing", "good"), ("Paused", "degraded"), ("Stopped", "bad")]} volume_delta = 10 @@ -117,6 +119,7 @@ class Meta: def post_config_hook(self): self.thresholds_init = self.py3.get_color_names_list(self.format_player) + self.replacements_init = self.py3.get_replacements_list(self.format_player) self.position = self.py3.format_contains(self.format_player, "position") self.cache_timeout = getattr(self, "cache_timeout", 1) @@ -283,6 +286,11 @@ def playerctl(self): if self.position and player_data["status"] == "Playing" and player_data["position"]: cached_until = self.cache_timeout + # Replace the values + for x in self.replacements_init: + if x in player_data: + player_data[x] = self.py3.replace(player_data[x], x) + # Set the color of a player for key in self.thresholds_init: if key in player_data: diff --git a/py3status/modules/spotify.py b/py3status/modules/spotify.py index 29b87e7a8a..4c7f358213 100644 --- a/py3status/modules/spotify.py +++ b/py3status/modules/spotify.py @@ -15,11 +15,8 @@ (default 'Spotify not running') format_stopped: define output if spotify is not playing (default 'Spotify stopped') - sanitize_titles: whether to remove meta data from album/track title - (default True) - sanitize_words: which meta data to remove - *(default ['bonus', 'demo', 'edit', 'explicit', 'extended', - 'feat', 'mono', 'remaster', 'stereo', 'version'])* + replacements: specify a list/dict of string placeholders to modify + (default None) Format placeholders: {album} album name @@ -45,6 +42,12 @@ button_previous = 5 format = "{title} by {artist} -> {time}" format_down = "no Spotify" + + # sanitize + replacements = { + "album": [("\s?[\(\[\-,;/][^)\],;/]*?(bonus|demo|edit|explicit|extended|feat|mono|remaster|stereo|version)[^)\],;/]*[\)\]]?", "")], + "title": [("\s?[\(\[\-,;/][^)\],;/]*?(bonus|demo|edit|explicit|extended|feat|mono|remaster|stereo|version)[^)\],;/]*[\)\]]?", "")] + } } ``` @@ -60,7 +63,6 @@ {'color': '#FF0000', 'full_text': 'Spotify stopped'} """ -import re from datetime import timedelta from time import sleep @@ -82,47 +84,11 @@ class Py3status: format = "{artist} : {title}" format_down = "Spotify not running" format_stopped = "Spotify stopped" - sanitize_titles = True - sanitize_words = [ - "bonus", - "demo", - "edit", - "explicit", - "extended", - "feat", - "mono", - "remaster", - "stereo", - "version", - ] + replacements = None def _spotify_cmd(self, action): return SPOTIFY_CMD.format(dbus_client=self.dbus_client, cmd=action) - def post_config_hook(self): - """ """ - # Match string after hyphen, comma, semicolon or slash containing any metadata word - # examples: - # - Remastered 2012 - # / Radio Edit - # ; Remastered - self.after_delimiter = self._compile_re(r"([\-,;/])([^\-,;/])*(META_WORDS_HERE).*") - - # Match brackets with their content containing any metadata word - # examples: - # (Remastered 2017) - # [Single] - # (Bonus Track) - self.inside_brackets = self._compile_re(r"([\(\[][^)\]]*?(META_WORDS_HERE)[^)\]]*?[\)\]])") - - def _compile_re(self, expression): - """ - Compile given regular expression for current sanitize words - """ - meta_words = "|".join(self.sanitize_words) - expression = expression.replace("META_WORDS_HERE", meta_words) - return re.compile(expression, re.IGNORECASE) - def _get_playback_status(self): """ Get the playback status. One of: "Playing", "Paused" or "Stopped". @@ -145,10 +111,6 @@ def _get_text(self): microtime = metadata.get("mpris:length") rtime = str(timedelta(seconds=microtime // 1_000_000)) title = metadata.get("xesam:title") - if self.sanitize_titles: - album = self._sanitize_title(album) - title = self._sanitize_title(title) - playback_status = self._get_playback_status() if playback_status == "Playing": color = self.py3.COLOR_PLAYING or self.py3.COLOR_GOOD @@ -160,29 +122,24 @@ def _get_text(self): self.py3.COLOR_PAUSED or self.py3.COLOR_DEGRADED, ) - return ( - self.py3.safe_format( - self.format, - dict( - title=title, - artist=artist, - album=album, - time=rtime, - playback=playback_status, - ), - ), - color, - ) + spotify_data = { + "title": title, + "artist": artist, + "album": album, + "time": rtime, + "playback": playback_status, + } + + for x in self.replacements_init: + if x in spotify_data: + spotify_data[x] = self.py3.replace(spotify_data[x], x) + + return (self.py3.safe_format(self.format, spotify_data), color) except Exception: return (self.format_down, self.py3.COLOR_OFFLINE or self.py3.COLOR_BAD) - def _sanitize_title(self, title): - """ - Remove redundant metadata from title and return it - """ - title = re.sub(self.inside_brackets, "", title) - title = re.sub(self.after_delimiter, "", title) - return title.strip() + def post_config_hook(self): + self.replacements_init = self.py3.get_replacements_list(self.format) def spotify(self): """ From 9dd2f2151d23f713fe4c335813d327b0979a27c9 Mon Sep 17 00:00:00 2001 From: lasers Date: Mon, 23 Dec 2024 13:33:15 -0600 Subject: [PATCH 3/3] cmus, deadbeef, moc: add self.py3.replace helper --- py3status/modules/cmus.py | 35 +++++++++++++++++++++-------------- py3status/modules/deadbeef.py | 7 +++++++ py3status/modules/moc.py | 27 +++++++++++++++++---------- 3 files changed, 45 insertions(+), 24 deletions(-) diff --git a/py3status/modules/cmus.py b/py3status/modules/cmus.py index 11e3229a84..dc6f5b191a 100644 --- a/py3status/modules/cmus.py +++ b/py3status/modules/cmus.py @@ -16,6 +16,7 @@ *(default '[\?if=is_started [\?if=is_playing > ][\?if=is_paused \|\| ]' '[\?if=is_stopped .. ][[{artist}][\?soft - ][{title}]' '|\?show cmus: waiting for user input]]')* + replacements: specify a list/dict of string placeholders to modify (default None) sleep_timeout: sleep interval for this module. when cmus is not running, this interval will be used. this allows some flexible timing where one might want to refresh constantly with some placeholders... or to refresh @@ -100,6 +101,7 @@ class Py3status: r"[\?if=is_stopped .. ][[{artist}][\?soft - ][{title}]" r"|\?show cmus: waiting for user input]]" ) + replacements = None sleep_timeout = 20 def post_config_hook(self): @@ -109,6 +111,7 @@ def post_config_hook(self): self.color_stopped = self.py3.COLOR_STOPPED or self.py3.COLOR_BAD self.color_paused = self.py3.COLOR_PAUSED or self.py3.COLOR_DEGRADED self.color_playing = self.py3.COLOR_PLAYING or self.py3.COLOR_GOOD + self.replacements_init = self.py3.get_replacements_list(self.format) def _seconds_to_time(self, value): m, s = divmod(int(value), 60) @@ -165,14 +168,14 @@ def cmus(self): cached_until = self.sleep_timeout color = self.py3.COLOR_BAD - is_started, data = self._get_cmus_data() + is_started, cmus_data = self._get_cmus_data() if is_started: cached_until = self.cache_timeout - data = self._organize_data(data) - data = self._manipulate_data(data) + cmus_data = self._organize_data(cmus_data) + cmus_data = self._manipulate_data(cmus_data) - status = data.get("status") + status = cmus_data.get("status") if status == "playing": is_playing = True color = self.color_playing @@ -183,19 +186,23 @@ def cmus(self): is_stopped = True color = self.color_stopped + for x in self.replacements_init: + if x in cmus_data: + cmus_data[x] = self.py3.replace(cmus_data[x], x) + + cmus_data.update( + { + "is_paused": is_paused, + "is_playing": is_playing, + "is_started": is_started, + "is_stopped": is_stopped, + } + ) + return { "cached_until": self.py3.time_in(cached_until), "color": color, - "full_text": self.py3.safe_format( - self.format, - dict( - is_paused=is_paused, - is_playing=is_playing, - is_started=is_started, - is_stopped=is_stopped, - **data, - ), - ), + "full_text": self.py3.safe_format(self.format, cmus_data), } def on_click(self, event): diff --git a/py3status/modules/deadbeef.py b/py3status/modules/deadbeef.py index e20ddb9d17..50e1a0f38f 100644 --- a/py3status/modules/deadbeef.py +++ b/py3status/modules/deadbeef.py @@ -4,6 +4,7 @@ Configuration parameters: cache_timeout: refresh interval for this module (default 5) format: display format for this module (default '[{artist} - ][{title}]') + replacements: specify a list/dict of string placeholders to modify (default None) sleep_timeout: when deadbeef is not running, this interval will be used to allow faster refreshes with time-related placeholders and/or to refresh few times per minute rather than every few seconds @@ -58,6 +59,7 @@ class Py3status: # available configuration parameters cache_timeout = 5 format = "[{artist} - ][{title}]" + replacements = None sleep_timeout = 20 class Meta: @@ -89,6 +91,7 @@ def post_config_hook(self): self.color_paused = self.py3.COLOR_PAUSED or self.py3.COLOR_DEGRADED self.color_playing = self.py3.COLOR_PLAYING or self.py3.COLOR_GOOD self.color_stopped = self.py3.COLOR_STOPPED or self.py3.COLOR_BAD + self.replacements_init = self.py3.get_replacements_list(self.format) def _is_running(self): try: @@ -124,6 +127,10 @@ def deadbeef(self): else: color = self.color_paused + for x in self.replacements_init: + if x in beef_data: + beef_data[x] = self.py3.replace(beef_data[x], x) + return { "cached_until": self.py3.time_in(cached_until), "full_text": self.py3.safe_format(self.format, beef_data), diff --git a/py3status/modules/moc.py b/py3status/modules/moc.py index 585e962be1..21b53bdd96 100644 --- a/py3status/modules/moc.py +++ b/py3status/modules/moc.py @@ -14,6 +14,7 @@ format: display format for this module *(default '\?if=is_started [\?if=is_stopped \[\] moc|' '[\?if=is_paused \|\|][\?if=is_playing >] {title}]')* + replacements: specify a list/dict of string placeholders to modify (default None) sleep_timeout: when moc is not running, this interval will be used to allow one to refresh constantly with time placeholders and/or to refresh once every minute rather than every few seconds @@ -88,6 +89,7 @@ class Py3status: r"\?if=is_started [\?if=is_stopped \[\] moc|" r"[\?if=is_paused \|\|][\?if=is_playing >] {title}]" ) + replacements = None sleep_timeout = 20 def post_config_hook(self): @@ -97,6 +99,7 @@ def post_config_hook(self): self.color_stopped = self.py3.COLOR_STOPPED or self.py3.COLOR_BAD self.color_paused = self.py3.COLOR_PAUSED or self.py3.COLOR_DEGRADED self.color_playing = self.py3.COLOR_PLAYING or self.py3.COLOR_GOOD + self.replacements_init = self.py3.get_replacements_list(self.format) def _get_moc_data(self): try: @@ -133,19 +136,23 @@ def moc(self): is_stopped = True color = self.color_stopped + for x in self.replacements_init: + if x in moc_data: + moc_data[x] = self.py3.replace(moc_data[x], x) + + moc_data.update( + { + "is_paused": is_paused, + "is_playing": is_playing, + "is_started": is_started, + "is_stopped": is_stopped, + } + ) + return { "cached_until": self.py3.time_in(cached_until), "color": color, - "full_text": self.py3.safe_format( - self.format, - dict( - is_paused=is_paused, - is_playing=is_playing, - is_started=is_started, - is_stopped=is_stopped, - **data, - ), - ), + "full_text": self.py3.safe_format(self.format, moc_data), } def on_click(self, event):