diff --git a/config/scripts/mel b/config/scripts/mel index 9c41cfd..ff96bd1 100755 --- a/config/scripts/mel +++ b/config/scripts/mel @@ -29,6 +29,7 @@ import email.message import email.parser import html import logging +import mailcap import os import pdb import re @@ -45,6 +46,7 @@ import progressbar import xdg.BaseDirectory MailLocation = typing.NewType('MailLocation', typing.Tuple[str, str, str]) +# MessageAction = typing.Callable[[notmuch.Message], None] class MelEngine: @@ -82,7 +84,7 @@ class MelEngine: self.accounts[name] = section def __init__(self, config_path: str) -> None: - self.log = logging.getLogger("Mel") + self.log = logging.getLogger("MelEngine") self.config = self.load_config(config_path) @@ -93,7 +95,6 @@ class MelEngine: # All the emails the user is represented as: self.aliases: typing.Set[str] = set() # TODO If the user send emails to himself, maybe that wont cut it. - self.mailbox_colors: typing.Dict[str, str] = dict() self.generate_aliases() @@ -179,25 +180,6 @@ class MelEngine: assert state in {'cur', 'tmp', 'new'} return (mailbox, folder, state) - def get_mailbox_color(self, mailbox: str) -> str: - """ - Return the color of the given mailbox in a ready to print - string with ASCII escape codes. - """ - assert self.config - if mailbox not in self.mailbox_colors: - # RGB colors (not supported everywhere) - # color_str = self.config[mailbox]["color"] - # color_str = color_str[1:] if color_str[0] == '#' else color_str - # R = int(color_str[0:2], 16) - # G = int(color_str[2:4], 16) - # B = int(color_str[4:6], 16) - # self.mailbox_colors[mailbox] = f"\x1b[38;2;{R};{G};{B}m" - color_int = int(self.config[mailbox]["color16"]) - - self.mailbox_colors[mailbox] = f"\x1b[38;5;{color_int}m" - return self.mailbox_colors[mailbox] - @staticmethod def is_uid(uid: typing.Any) -> bool: """ @@ -273,7 +255,6 @@ class MelEngine: *args: typing.Any, show_progress: bool = False, write: bool = False, close_db: bool = True, **kwargs: typing.Any) -> int: - # TODO Detail the typing.Callable """ Run a function on the messages selected by the given query. """ @@ -286,11 +267,12 @@ class MelEngine: elements = query.search_messages() nb_msgs = query.count_messages() - iterator = progressbar.progressbar( - elements, max_value=nb_msgs) if show_progress else elements + iterator = progressbar.progressbar(elements, max_value=nb_msgs) \ + if show_progress and nb_msgs else elements self.log.info("Executing %s", action) for msg in iterator: + self.log.debug("On mail %s", msg) if write: msg.freeze() @@ -328,12 +310,18 @@ class MelOutput: return (None, None) def __init__(self, engine: MelEngine) -> None: + colorama.init() self.log = logging.getLogger("MelOutput") self.engine = engine + self.light_background = True self.is_tty = sys.stdout.isatty() self.dest_width, self.subject_width = self.compute_line_format() + self.mailbox_colors: typing.Dict[str, str] = dict() + + # TODO Allow custom path + self.caps = mailcap.getcaps() @staticmethod def format_date(date: datetime.datetime) -> str: @@ -345,6 +333,10 @@ class MelOutput: now = datetime.datetime.now() if now - date < datetime.timedelta(days=1): return date.strftime('%H:%M:%S') + if now - date < datetime.timedelta(days=28): + return date.strftime('%d %H:%M') + if now - date < datetime.timedelta(days=365): + return date.strftime('%m-%d %H') return date.strftime('%y-%m-%d') @staticmethod @@ -383,6 +375,26 @@ class MelOutput: remainder /= 1024.0 return "%.1f %s%s" % (remainder, 'Yi', suffix) + def get_mailbox_color(self, mailbox: str) -> str: + """ + Return the color of the given mailbox in a ready to print + string with ASCII escape codes. + """ + if not self.is_tty: + return '' + if mailbox not in self.mailbox_colors: + # RGB colors (not supported everywhere) + # color_str = self.config[mailbox]["color"] + # color_str = color_str[1:] if color_str[0] == '#' else color_str + # R = int(color_str[0:2], 16) + # G = int(color_str[2:4], 16) + # B = int(color_str[4:6], 16) + # self.mailbox_colors[mailbox] = f"\x1b[38;2;{R};{G};{B}m" + color_int = int(self.engine.config[mailbox]["color16"]) + + self.mailbox_colors[mailbox] = f"\x1b[38;5;{color_int}m" + return self.mailbox_colors[mailbox] + def print_msg(self, msg: notmuch.Message) -> None: """ Print the given message header on one line. @@ -394,25 +406,33 @@ class MelOutput: line = "" tags = set(msg.get_tags()) mailbox, _, _ = self.engine.get_location(msg) - if self.is_tty: - line += self.engine.get_mailbox_color(mailbox) + if 'unread' in tags or 'flagged' in tags: + line += colorama.Style.BRIGHT + # if 'flagged' in tags: + # line += colorama.Style.BRIGHT + # if 'unread' not in tags: + # line += colorama.Style.DIM + line += colorama.Back.LIGHTBLACK_EX if self.light_background \ + else colorama.Back.BLACK + self.light_background = not self.light_background + line += self.get_mailbox_color(mailbox) # UID uid = None for tag in tags: if tag.startswith('tuid'): uid = tag[4:] - assert uid and MelEngine.is_uid( - uid), "{uid} ({type(UID)}) is not a valid UID." + assert uid, f"No UID for message: {msg}." + assert MelEngine.is_uid(uid), f"{uid} {type(uid)} is not a valid UID." line += uid # Date - line += sep + line += sep + colorama.Fore.MAGENTA date = datetime.datetime.fromtimestamp(msg.get_date()) line += self.format_date(date) # Icons - line += sep + line += sep + colorama.Fore.RED def tags2col1(tag1: str, tag2: str, characters: typing.Tuple[str, str, str, str]) -> None: @@ -437,15 +457,16 @@ class MelOutput: tags2col1('unread', 'flagged', ('!', 'U', 'F', ' ')) tags2col1('sent', 'replied', ('?', '↑', '↪', ' ')) + # Opposed + line += sep + colorama.Fore.BLUE if 'sent' in tags: dest = msg.get_header("to") else: dest = msg.get_header("from") - line += sep line += MelOutput.clip_text(self.dest_width, dest) # Subject - line += sep + line += sep + colorama.Fore.WHITE subject = msg.get_header("subject") line += MelOutput.clip_text(self.subject_width, subject) @@ -560,11 +581,41 @@ class MelOutput: print() # Show text/plain + # TODO Consider alternative for part in mail.walk(): + if part.is_multipart(): + continue + payl = part.get_payload(decode=True) + assert isinstance(payl, bytes) if part.get_content_type() == "text/plain": - payl = part.get_payload(decode=True) - assert isinstance(payl, bytes) print(payl.decode()) + else: + # TODO Use nametemplate from mailcap + temp_file = '/tmp/melcap.html' # TODO Real temporary file + # TODO FIFO if possible + with open(temp_file, 'wb') as temp_filedesc: + temp_filedesc.write(payl) + command, _ = mailcap.findmatch( + self.caps, part.get_content_type(), key='view', filename=temp_file) + if command: + os.system(command) + + def print_dir_list(self) -> None: + """ + Print a colored directory list. + Every line is easilly copiable. + """ + for arb in self.engine.list_folders(): + line = colorama.Fore.LIGHTBLACK_EX + "'" + line += self.get_mailbox_color(arb[0]) + line += arb[0].replace("'", "\\'") + line += colorama.Fore.LIGHTBLACK_EX + for inter in arb[1:-1]: + line += '/' + inter.replace("'", "\\'") + line += '/' + colorama.Fore.WHITE + arb[-1].replace("'", "\\'") + line += colorama.Fore.LIGHTBLACK_EX + "'" + line += colorama.Style.RESET_ALL + print(line) class MelCLI(): @@ -606,11 +657,12 @@ class MelCLI(): for message in messages: query_str = f'tag:tuid{message}' - nb_msgs = self.engine.apply_msgs( - query_str, action, write=write, close_db=False) + nb_msgs = self.engine.apply_msgs(query_str, action, + write=write, close_db=False) if nb_msgs < 1: self.log.error( "Couldn't execute function for message %s", message) + self.engine.close_database() def operation_default(self) -> None: """ @@ -663,7 +715,7 @@ class MelCLI(): mbsync_config_file = os.path.expanduser( "~/.config/mbsyncrc") # TODO Better cmd = ["mbsync", "--config", mbsync_config_file, "--all"] - subprocess.run(cmd, check=True) + subprocess.run(cmd, check=False) # Index new mails self.engine.notmuch_new() @@ -675,12 +727,17 @@ class MelCLI(): self.engine.apply_msgs('tag:unprocessed', self.engine.retag_msg, show_progress=True, write=True) + def operation_list(self) -> None: + """ + List operation: Print all folders. + """ + self.output.print_dir_list() + def operation_debug(self) -> None: """ DEBUG """ - from pprint import pprint - pprint(self.engine.list_folders()) + print("UwU") def operation_retag(self) -> None: """ @@ -717,6 +774,10 @@ class MelCLI(): # list folder [--recurse] # List actions + parser_list = subparsers.add_parser( + "list", help="List all folders") + # parser_list.add_argument('message', nargs='*', help="Messages") + parser_list.set_defaults(operation=self.operation_list) # flag msg... parser_flag = subparsers.add_parser( @@ -811,10 +872,12 @@ class MelCLI(): if __name__ == "__main__": - colorama.init() - try: + if not os.environ.get("MEL_DEBUG"): CLI = MelCLI() - except: - EXTYPE, VALUE, TB = sys.exc_info() - traceback.print_exc() - pdb.post_mortem(TB) + else: + try: + CLI = MelCLI() + except: + EXTYPE, VALUE, TB = sys.exc_info() + traceback.print_exc() + pdb.post_mortem(TB) diff --git a/config/scripts/music_remove_dashes b/config/scripts/music_remove_dashes new file mode 100755 index 0000000..2a1ba1e --- /dev/null +++ b/config/scripts/music_remove_dashes @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +""" +Small script to convert music files in the form: +$(tracknumber) - $(title).$(ext) +to the form +$(tracknumber) $(title).$(ext) +(note the absence of dash) +""" + +import os +import re + + +def main() -> None: + """ + Function that executes the script. + """ + for root, _, files in os.walk('.'): + for filename in files: + match = re.match(r'^(\d+) - (.+)$', filename) + if not match: + continue + new_filename = f"{match[1]} {match[2]}" + old_path = os.path.join(root, filename) + new_path = os.path.join(root, new_filename) + print(old_path, '->', new_path) + os.rename(old_path, new_path) + + +if __name__ == '__main__': + main() diff --git a/config/scripts/optimize b/config/scripts/optimize index 6878a29..c2ef705 100755 --- a/config/scripts/optimize +++ b/config/scripts/optimize @@ -146,6 +146,23 @@ do done <<< "$(find "$dir/" -type f -iname "*.png")" +# FLAC (requires reflac) +while read music +do + if [ -z "$music" ]; then continue; fi + echo Processing $music + + temp_dir=$(mktemp --directory) + temp="$temp_dir/to_optimize.flac" + cp "$music" "$temp" + reflac --best "$temp_dir" + echo "→ Optimize done" + + replace "$temp" "$music" + rm -rf "$temp_dir" + +done <<< "$(find "$dir/" -type f -iname "*.flac")" + # # SVG (requires scour) # while read image # do diff --git a/config/scripts/replayGain b/config/scripts/replayGain index f9d7013..25545a8 100755 --- a/config/scripts/replayGain +++ b/config/scripts/replayGain @@ -3,6 +3,9 @@ # Normalisation is done at the default of each program, # which is usually -89.0 dB +# TODO The simplifications/fixes I've done makes it consider +# multi-discs albums as multiple albums + import logging import os import sys diff --git a/config/scripts/softwareList b/config/scripts/softwareList index dabeedd..c943535 100755 --- a/config/scripts/softwareList +++ b/config/scripts/softwareList @@ -152,6 +152,7 @@ then i notmuch # Index mail i neomutt || i mutt # CLI mail client i lynx # CLI web browser (for HTML mail) + i tiv # CLI image viewer if $INSTALL_GUI then i thunderbird # GUI mail client (just in case) @@ -175,6 +176,7 @@ then i duperemove # Find and merge dupplicates on BTRFS partitions i optipng # Optimize PNG files i jpegtran libjpeg-turbo # Compress JPEG files + i reflac # Recompress FLAC files fi