diff --git a/py3status/constants.py b/py3status/constants.py index 0a03a9b75a..c4e7103e73 100644 --- a/py3status/constants.py +++ b/py3status/constants.py @@ -270,3 +270,16 @@ MARKUP_LANGUAGES = ["pango", "none"] ON_ERROR_VALUES = ["hide", "show"] + +DEFAULT_SANITIZE_WORDS = [ + "bonus", + "demo", + "edit", + "explicit", + "extended", + "feat", + "mono", + "remaster", + "stereo", + "version", +] diff --git a/py3status/modules/cmus.py b/py3status/modules/cmus.py index 11e3229a84..45da8b9659 100644 --- a/py3status/modules/cmus.py +++ b/py3status/modules/cmus.py @@ -16,6 +16,11 @@ *(default '[\?if=is_started [\?if=is_playing > ][\?if=is_paused \|\| ]' '[\?if=is_stopped .. ][[{artist}][\?soft - ][{title}]' '|\?show cmus: waiting for user input]]')* + 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'])* 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 @@ -83,6 +88,8 @@ {'color': '#FF0000', 'full_text': '.. cmus: waiting for user input'} """ +from py3status.constants import DEFAULT_SANITIZE_WORDS + STRING_NOT_INSTALLED = "not installed" @@ -100,6 +107,8 @@ class Py3status: r"[\?if=is_stopped .. ][[{artist}][\?soft - ][{title}]" r"|\?show cmus: waiting for user input]]" ) + sanitize_titles = True + sanitize_words = DEFAULT_SANITIZE_WORDS sleep_timeout = 20 def post_config_hook(self): @@ -149,6 +158,9 @@ def _manipulate_data(self, data): temporary[key] = True elif value in ("false", "disabled"): temporary[key] = False + # sanitize album and title + elif self.sanitize_titles and key in ("album", "title"): + temporary[key] = self.py3.sanitize_title(self.sanitize_words, value) # string not modified else: temporary[key] = value diff --git a/py3status/modules/deadbeef.py b/py3status/modules/deadbeef.py index e20ddb9d17..a45f49c504 100644 --- a/py3status/modules/deadbeef.py +++ b/py3status/modules/deadbeef.py @@ -4,6 +4,11 @@ Configuration parameters: cache_timeout: refresh interval for this module (default 5) format: display format for this module (default '[{artist} - ][{title}]') + 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'])* 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 @@ -49,6 +54,8 @@ {'color': '#ffff00', 'full_text': 'Music For Programming - Lackluster'} """ +from py3status.constants import DEFAULT_SANITIZE_WORDS + STRING_NOT_INSTALLED = "not installed" @@ -58,6 +65,8 @@ class Py3status: # available configuration parameters cache_timeout = 5 format = "[{artist} - ][{title}]" + sanitize_titles = True + sanitize_words = DEFAULT_SANITIZE_WORDS sleep_timeout = 20 class Meta: @@ -119,6 +128,14 @@ def deadbeef(self): beef_data = dict(zip(self.placeholders, line.split(self.separator))) cached_until = self.cache_timeout + # Sanitize album and title + if self.sanitize_titles: + if "album" in beef_data: + beef_data["album"] = self.py3.sanitize_title(self.sanitize_words, beef_data["album"]) + + if "title" in beef_data: + beef_data["title"] = self.py3.sanitize_title(self.sanitize_words, beef_data["title"]) + if beef_data["isplaying"]: color = self.color_playing else: diff --git a/py3status/modules/moc.py b/py3status/modules/moc.py index 585e962be1..399dcd25bd 100644 --- a/py3status/modules/moc.py +++ b/py3status/modules/moc.py @@ -14,6 +14,11 @@ format: display format for this module *(default '\?if=is_started [\?if=is_stopped \[\] moc|' '[\?if=is_paused \|\|][\?if=is_playing >] {title}]')* + 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'])* 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 @@ -72,6 +77,8 @@ {'color': '#FF0000', 'full_text': '[] moc'} """ +from py3status.constants import DEFAULT_SANITIZE_WORDS + STRING_NOT_INSTALLED = "not installed" @@ -88,6 +95,8 @@ class Py3status: r"\?if=is_started [\?if=is_stopped \[\] moc|" r"[\?if=is_paused \|\|][\?if=is_playing >] {title}]" ) + sanitize_titles = True + sanitize_words = DEFAULT_SANITIZE_WORDS sleep_timeout = 20 def post_config_hook(self): @@ -122,6 +131,14 @@ def moc(self): category, value = line.split(": ", 1) data[category.lower()] = value + # Sanitize album and title + if self.sanitize_titles: + if "album" in data: + data["album"] = self.py3.sanitize_title(self.sanitize_words, data["album"]) + + if "title" in data: + data["title"] = self.py3.sanitize_title(self.sanitize_words, data["title"]) + self.state = data["state"] if self.state == "PLAY": is_playing = True diff --git a/py3status/modules/mpd_status.py b/py3status/modules/mpd_status.py index fbf73f8bb0..303965c502 100644 --- a/py3status/modules/mpd_status.py +++ b/py3status/modules/mpd_status.py @@ -19,6 +19,11 @@ max_width: maximum status length (default 120) password: mpd password (default None) port: mpd port (default '6600') + 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'])* state_pause: label to display for "paused" state (default '[pause]') state_play: label to display for "playing" state (default '[play]') state_stop: label to display for "stopped" state (default '[stop]') @@ -74,33 +79,7 @@ from mpd import CommandError, ConnectionError, MPDClient from py3status.composite import Composite - - -def song_attr(song, attr): - def parse_mtime(date_str): - return datetime.datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%SZ") - - if attr == "time": - try: - duration = int(song["time"]) - if duration > 0: - minutes, seconds = divmod(duration, 60) - return f"{minutes:d}:{seconds:02d}" - raise ValueError - except (KeyError, ValueError): - return "" - elif attr == "position": - try: - return "{}".format(int(song["pos"]) + 1) - except (KeyError, ValueError): - return "" - elif attr == "mtime": - return parse_mtime(song["last-modified"]).strftime("%c") - elif attr == "mdate": - return parse_mtime(song["last-modified"]).strftime("%x") - - return song.get(attr, "") - +from py3status.constants import DEFAULT_SANITIZE_WORDS class Py3status: """ """ @@ -117,6 +96,8 @@ class Py3status: max_width = 120 password = None port = "6600" + sanitize_titles = True + sanitize_words = DEFAULT_SANITIZE_WORDS state_pause = "[pause]" state_play = "[play]" state_stop = "[stop]" @@ -133,6 +114,34 @@ def post_config_hook(self): self.current_status = None self.idle_thread = Thread() + def _song_attr(self, song, attr): + def parse_mtime(date_str): + return datetime.datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%SZ") + + if attr == "time": + try: + duration = int(song["time"]) + if duration > 0: + minutes, seconds = divmod(duration, 60) + return f"{minutes:d}:{seconds:02d}" + raise ValueError + except (KeyError, ValueError): + return "" + elif attr == "position": + try: + return "{}".format(int(song["pos"]) + 1) + except (KeyError, ValueError): + return "" + elif attr == "mtime": + return parse_mtime(song["last-modified"]).strftime("%c") + elif attr == "mdate": + return parse_mtime(song["last-modified"]).strftime("%x") + # Sanitize album and title + elif self.sanitize_titles and attr in ("album", "title"): + return self.py3.sanitize_title(self.sanitize_words, song.get(attr, "")) + + return song.get(attr, "") + def _get_mpd(self, disconnect=False): if disconnect: try: @@ -229,8 +238,8 @@ def _get_status(self): def attr_getter(attr): if attr.startswith("next_"): - return song_attr(next_song, attr[5:]) - return song_attr(song, attr) + return self._song_attr(next_song, attr[5:]) + return self._song_attr(song, attr) text = self.py3.safe_format(self.format, attr_getter=attr_getter) if isinstance(text, Composite): @@ -277,4 +286,4 @@ def kill(self): """ from py3status.module_test import module_test - module_test(Py3status) + module_test(Py3status, config={"format": "{artist} - {title}"}) diff --git a/py3status/modules/mpris.py b/py3status/modules/mpris.py index bd58555a47..8f1006f717 100644 --- a/py3status/modules/mpris.py +++ b/py3status/modules/mpris.py @@ -29,13 +29,18 @@ Keep in mind that the state has a higher priority than player_priority. So when player_priority is "[mpd, bomi]" and mpd is paused and bomi is playing than bomi wins. (default []) + 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'])* state_pause: specify icon for pause state (default u'\u25eb') state_play: specify icon for play state (default u'\u25b7') state_stop: specify icon for stop state (default u'\u25a1') Format placeholders: {album} album name - {artist} artiste name (first one) + {artist} artist name (first one) {length} time duration of the song {player} show name of the player {player_shortname} show name of the player from busname (usually command line name) @@ -117,6 +122,7 @@ from mpris2 import get_players_uri from mpris2.types import Metadata_Map +from py3status.constants import DEFAULT_SANITIZE_WORDS class STATE(IntEnum): Playing = 0 @@ -282,6 +288,14 @@ def metadata(self, metadata=None): self._metadata["nowplaying"] = metadata.get("vlc:nowplaying", None) + # Sanitize album and title + if self.parent.sanitize_titles: + if "album" in self._metadata: + self._metadata["album"] = self.parent.py3.sanitize_title(self.parent.sanitize_words, self._metadata["album"]) + + if "title" in self._metadata: + self._metadata["title"] = self.parent.py3.sanitize_title(self.parent.sanitize_words, self._metadata["title"]) + if not self._metadata.get("title"): self._metadata["title"] = "No Track" @@ -382,6 +396,8 @@ class Py3status: max_width = None player_hide_non_canplay = [] player_priority = [] + sanitize_titles = True + sanitize_words = DEFAULT_SANITIZE_WORDS state_pause = "\u25eb" state_play = "\u25b7" state_stop = "\u25a1" diff --git a/py3status/modules/playerctl.py b/py3status/modules/playerctl.py index 605317a884..a023866940 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,11 @@ '[\?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 []) + 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'])* 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")]}) @@ -73,6 +78,8 @@ import gi from gi.repository import GLib, Playerctl +from py3status.constants import DEFAULT_SANITIZE_WORDS + gi.require_version("Playerctl", "2.0") @@ -99,6 +106,8 @@ class Py3status: ) format_player_separator = " " players = [] + sanitize_titles = True + sanitize_words = DEFAULT_SANITIZE_WORDS seek_delta = 5 thresholds = {"status": [("Playing", "good"), ("Paused", "degraded"), ("Stopped", "bad")]} volume_delta = 10 @@ -234,6 +243,11 @@ def _get_player_data(self, player): data["status"] = player.props.status data["volume"] = int(player.props.volume * 100) + # Sanitize album and title + if self.sanitize_titles: + data["album"] = self.py3.sanitize_title(self.sanitize_words, data["album"]) + data["title"] = self.py3.sanitize_title(self.sanitize_words, data["title"]) + return data def _get_player_from_index(self, index): diff --git a/py3status/modules/spotify.py b/py3status/modules/spotify.py index 29b87e7a8a..dc5ea28005 100644 --- a/py3status/modules/spotify.py +++ b/py3status/modules/spotify.py @@ -23,7 +23,7 @@ Format placeholders: {album} album name - {artist} artiste name (first one) + {artist} artist name (first one) {playback} state of the playback: Playing, Paused {time} time duration of the song {title} name of the song @@ -60,12 +60,13 @@ {'color': '#FF0000', 'full_text': 'Spotify stopped'} """ -import re from datetime import timedelta from time import sleep import dbus +from py3status.constants import DEFAULT_SANITIZE_WORDS + SPOTIFY_CMD = """dbus-send --print-reply --dest={dbus_client} /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.{cmd}""" @@ -83,46 +84,11 @@ class Py3status: format_down = "Spotify not running" format_stopped = "Spotify stopped" sanitize_titles = True - sanitize_words = [ - "bonus", - "demo", - "edit", - "explicit", - "extended", - "feat", - "mono", - "remaster", - "stereo", - "version", - ] + sanitize_words = DEFAULT_SANITIZE_WORDS 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,9 +111,11 @@ def _get_text(self): microtime = metadata.get("mpris:length") rtime = str(timedelta(seconds=microtime // 1_000_000)) title = metadata.get("xesam:title") + + # Sanitize album and title if self.sanitize_titles: - album = self._sanitize_title(album) - title = self._sanitize_title(title) + album = self.py3.sanitize_title(self.sanitize_words, album) + title = self.py3.sanitize_title(self.sanitize_words, title) playback_status = self._get_playback_status() if playback_status == "Playing": @@ -176,14 +144,6 @@ def _get_text(self): 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 spotify(self): """ Get the current "artist - title" and return it. diff --git a/py3status/py3.py b/py3status/py3.py index 8550f89f1b..d573f150ed 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._sanitize_title_regex_cache = {} self._report_exception_cache = set() self._thresholds = None self._threshold_gradients = {} @@ -1291,3 +1293,36 @@ def get_http_response(): self.log(f"HTTP request retry {retry_times}/{retry_times}") sleep(retry_wait) return get_http_response() + + def sanitize_title(self, words, title): + """ + Remove the provided redundant metadata from the title and return it + """ + + # If our title is None or an empty string, we can return early + if title is None or title == "": + return title + + meta_words = "|".join(words) + + # If our regexes haven't already been compiled, do that and store them in the cache + if meta_words not in self._sanitize_title_regex_cache: + self._sanitize_title_regex_cache[meta_words] = { + # Match brackets with their content containing any metadata word + # examples: + # (Remastered 2017) + # [Single] + # (Bonus Track) + "inside_brackets": re.compile(r"([\(\[][^)\]]*?({})[^)\]]*?[\)\]])".format(meta_words), re.IGNORECASE), + # Match string after hyphen, comma, semicolon or slash containing any metadata word + # examples: + # - Remastered 2012 + # / Radio Edit + # ; Remastered + "after_delimiter": re.compile(r"([\-,;/])([^\-,;/])*({}).*".format(meta_words), re.IGNORECASE), + } + + title = re.sub(self._sanitize_title_regex_cache[meta_words]["inside_brackets"], "", title) + title = re.sub(self._sanitize_title_regex_cache[meta_words]["after_delimiter"], "", title) + + return title.strip()