Mite
This commit is contained in:
parent
90ca29d35c
commit
45058b4272
|
@ -29,6 +29,7 @@ import email.message
|
||||||
import email.parser
|
import email.parser
|
||||||
import html
|
import html
|
||||||
import logging
|
import logging
|
||||||
|
import mailcap
|
||||||
import os
|
import os
|
||||||
import pdb
|
import pdb
|
||||||
import re
|
import re
|
||||||
|
@ -45,6 +46,7 @@ import progressbar
|
||||||
import xdg.BaseDirectory
|
import xdg.BaseDirectory
|
||||||
|
|
||||||
MailLocation = typing.NewType('MailLocation', typing.Tuple[str, str, str])
|
MailLocation = typing.NewType('MailLocation', typing.Tuple[str, str, str])
|
||||||
|
# MessageAction = typing.Callable[[notmuch.Message], None]
|
||||||
|
|
||||||
|
|
||||||
class MelEngine:
|
class MelEngine:
|
||||||
|
@ -82,7 +84,7 @@ class MelEngine:
|
||||||
self.accounts[name] = section
|
self.accounts[name] = section
|
||||||
|
|
||||||
def __init__(self, config_path: str) -> None:
|
def __init__(self, config_path: str) -> None:
|
||||||
self.log = logging.getLogger("Mel")
|
self.log = logging.getLogger("MelEngine")
|
||||||
|
|
||||||
self.config = self.load_config(config_path)
|
self.config = self.load_config(config_path)
|
||||||
|
|
||||||
|
@ -93,7 +95,6 @@ class MelEngine:
|
||||||
# All the emails the user is represented as:
|
# All the emails the user is represented as:
|
||||||
self.aliases: typing.Set[str] = set()
|
self.aliases: typing.Set[str] = set()
|
||||||
# TODO If the user send emails to himself, maybe that wont cut it.
|
# TODO If the user send emails to himself, maybe that wont cut it.
|
||||||
self.mailbox_colors: typing.Dict[str, str] = dict()
|
|
||||||
|
|
||||||
self.generate_aliases()
|
self.generate_aliases()
|
||||||
|
|
||||||
|
@ -179,25 +180,6 @@ class MelEngine:
|
||||||
assert state in {'cur', 'tmp', 'new'}
|
assert state in {'cur', 'tmp', 'new'}
|
||||||
return (mailbox, folder, state)
|
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
|
@staticmethod
|
||||||
def is_uid(uid: typing.Any) -> bool:
|
def is_uid(uid: typing.Any) -> bool:
|
||||||
"""
|
"""
|
||||||
|
@ -273,7 +255,6 @@ class MelEngine:
|
||||||
*args: typing.Any, show_progress: bool = False,
|
*args: typing.Any, show_progress: bool = False,
|
||||||
write: bool = False, close_db: bool = True,
|
write: bool = False, close_db: bool = True,
|
||||||
**kwargs: typing.Any) -> int:
|
**kwargs: typing.Any) -> int:
|
||||||
# TODO Detail the typing.Callable
|
|
||||||
"""
|
"""
|
||||||
Run a function on the messages selected by the given query.
|
Run a function on the messages selected by the given query.
|
||||||
"""
|
"""
|
||||||
|
@ -286,11 +267,12 @@ class MelEngine:
|
||||||
elements = query.search_messages()
|
elements = query.search_messages()
|
||||||
nb_msgs = query.count_messages()
|
nb_msgs = query.count_messages()
|
||||||
|
|
||||||
iterator = progressbar.progressbar(
|
iterator = progressbar.progressbar(elements, max_value=nb_msgs) \
|
||||||
elements, max_value=nb_msgs) if show_progress else elements
|
if show_progress and nb_msgs else elements
|
||||||
|
|
||||||
self.log.info("Executing %s", action)
|
self.log.info("Executing %s", action)
|
||||||
for msg in iterator:
|
for msg in iterator:
|
||||||
|
self.log.debug("On mail %s", msg)
|
||||||
if write:
|
if write:
|
||||||
msg.freeze()
|
msg.freeze()
|
||||||
|
|
||||||
|
@ -328,12 +310,18 @@ class MelOutput:
|
||||||
return (None, None)
|
return (None, None)
|
||||||
|
|
||||||
def __init__(self, engine: MelEngine) -> None:
|
def __init__(self, engine: MelEngine) -> None:
|
||||||
|
colorama.init()
|
||||||
self.log = logging.getLogger("MelOutput")
|
self.log = logging.getLogger("MelOutput")
|
||||||
|
|
||||||
self.engine = engine
|
self.engine = engine
|
||||||
|
|
||||||
|
self.light_background = True
|
||||||
self.is_tty = sys.stdout.isatty()
|
self.is_tty = sys.stdout.isatty()
|
||||||
self.dest_width, self.subject_width = self.compute_line_format()
|
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
|
@staticmethod
|
||||||
def format_date(date: datetime.datetime) -> str:
|
def format_date(date: datetime.datetime) -> str:
|
||||||
|
@ -345,6 +333,10 @@ class MelOutput:
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
if now - date < datetime.timedelta(days=1):
|
if now - date < datetime.timedelta(days=1):
|
||||||
return date.strftime('%H:%M:%S')
|
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')
|
return date.strftime('%y-%m-%d')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -383,6 +375,26 @@ class MelOutput:
|
||||||
remainder /= 1024.0
|
remainder /= 1024.0
|
||||||
return "%.1f %s%s" % (remainder, 'Yi', suffix)
|
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:
|
def print_msg(self, msg: notmuch.Message) -> None:
|
||||||
"""
|
"""
|
||||||
Print the given message header on one line.
|
Print the given message header on one line.
|
||||||
|
@ -394,25 +406,33 @@ class MelOutput:
|
||||||
line = ""
|
line = ""
|
||||||
tags = set(msg.get_tags())
|
tags = set(msg.get_tags())
|
||||||
mailbox, _, _ = self.engine.get_location(msg)
|
mailbox, _, _ = self.engine.get_location(msg)
|
||||||
if self.is_tty:
|
if 'unread' in tags or 'flagged' in tags:
|
||||||
line += self.engine.get_mailbox_color(mailbox)
|
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
|
||||||
uid = None
|
uid = None
|
||||||
for tag in tags:
|
for tag in tags:
|
||||||
if tag.startswith('tuid'):
|
if tag.startswith('tuid'):
|
||||||
uid = tag[4:]
|
uid = tag[4:]
|
||||||
assert uid and MelEngine.is_uid(
|
assert uid, f"No UID for message: {msg}."
|
||||||
uid), "{uid} ({type(UID)}) is not a valid UID."
|
assert MelEngine.is_uid(uid), f"{uid} {type(uid)} is not a valid UID."
|
||||||
line += uid
|
line += uid
|
||||||
|
|
||||||
# Date
|
# Date
|
||||||
line += sep
|
line += sep + colorama.Fore.MAGENTA
|
||||||
date = datetime.datetime.fromtimestamp(msg.get_date())
|
date = datetime.datetime.fromtimestamp(msg.get_date())
|
||||||
line += self.format_date(date)
|
line += self.format_date(date)
|
||||||
|
|
||||||
# Icons
|
# Icons
|
||||||
line += sep
|
line += sep + colorama.Fore.RED
|
||||||
|
|
||||||
def tags2col1(tag1: str, tag2: str,
|
def tags2col1(tag1: str, tag2: str,
|
||||||
characters: typing.Tuple[str, str, str, str]) -> None:
|
characters: typing.Tuple[str, str, str, str]) -> None:
|
||||||
|
@ -437,15 +457,16 @@ class MelOutput:
|
||||||
tags2col1('unread', 'flagged', ('!', 'U', 'F', ' '))
|
tags2col1('unread', 'flagged', ('!', 'U', 'F', ' '))
|
||||||
tags2col1('sent', 'replied', ('?', '↑', '↪', ' '))
|
tags2col1('sent', 'replied', ('?', '↑', '↪', ' '))
|
||||||
|
|
||||||
|
# Opposed
|
||||||
|
line += sep + colorama.Fore.BLUE
|
||||||
if 'sent' in tags:
|
if 'sent' in tags:
|
||||||
dest = msg.get_header("to")
|
dest = msg.get_header("to")
|
||||||
else:
|
else:
|
||||||
dest = msg.get_header("from")
|
dest = msg.get_header("from")
|
||||||
line += sep
|
|
||||||
line += MelOutput.clip_text(self.dest_width, dest)
|
line += MelOutput.clip_text(self.dest_width, dest)
|
||||||
|
|
||||||
# Subject
|
# Subject
|
||||||
line += sep
|
line += sep + colorama.Fore.WHITE
|
||||||
subject = msg.get_header("subject")
|
subject = msg.get_header("subject")
|
||||||
line += MelOutput.clip_text(self.subject_width, subject)
|
line += MelOutput.clip_text(self.subject_width, subject)
|
||||||
|
|
||||||
|
@ -560,11 +581,41 @@ class MelOutput:
|
||||||
print()
|
print()
|
||||||
|
|
||||||
# Show text/plain
|
# Show text/plain
|
||||||
|
# TODO Consider alternative
|
||||||
for part in mail.walk():
|
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":
|
if part.get_content_type() == "text/plain":
|
||||||
payl = part.get_payload(decode=True)
|
|
||||||
assert isinstance(payl, bytes)
|
|
||||||
print(payl.decode())
|
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():
|
class MelCLI():
|
||||||
|
@ -606,11 +657,12 @@ class MelCLI():
|
||||||
|
|
||||||
for message in messages:
|
for message in messages:
|
||||||
query_str = f'tag:tuid{message}'
|
query_str = f'tag:tuid{message}'
|
||||||
nb_msgs = self.engine.apply_msgs(
|
nb_msgs = self.engine.apply_msgs(query_str, action,
|
||||||
query_str, action, write=write, close_db=False)
|
write=write, close_db=False)
|
||||||
if nb_msgs < 1:
|
if nb_msgs < 1:
|
||||||
self.log.error(
|
self.log.error(
|
||||||
"Couldn't execute function for message %s", message)
|
"Couldn't execute function for message %s", message)
|
||||||
|
self.engine.close_database()
|
||||||
|
|
||||||
def operation_default(self) -> None:
|
def operation_default(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -663,7 +715,7 @@ class MelCLI():
|
||||||
mbsync_config_file = os.path.expanduser(
|
mbsync_config_file = os.path.expanduser(
|
||||||
"~/.config/mbsyncrc") # TODO Better
|
"~/.config/mbsyncrc") # TODO Better
|
||||||
cmd = ["mbsync", "--config", mbsync_config_file, "--all"]
|
cmd = ["mbsync", "--config", mbsync_config_file, "--all"]
|
||||||
subprocess.run(cmd, check=True)
|
subprocess.run(cmd, check=False)
|
||||||
|
|
||||||
# Index new mails
|
# Index new mails
|
||||||
self.engine.notmuch_new()
|
self.engine.notmuch_new()
|
||||||
|
@ -675,12 +727,17 @@ class MelCLI():
|
||||||
self.engine.apply_msgs('tag:unprocessed', self.engine.retag_msg,
|
self.engine.apply_msgs('tag:unprocessed', self.engine.retag_msg,
|
||||||
show_progress=True, write=True)
|
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:
|
def operation_debug(self) -> None:
|
||||||
"""
|
"""
|
||||||
DEBUG
|
DEBUG
|
||||||
"""
|
"""
|
||||||
from pprint import pprint
|
print("UwU")
|
||||||
pprint(self.engine.list_folders())
|
|
||||||
|
|
||||||
def operation_retag(self) -> None:
|
def operation_retag(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -717,6 +774,10 @@ class MelCLI():
|
||||||
|
|
||||||
# list folder [--recurse]
|
# list folder [--recurse]
|
||||||
# List actions
|
# 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...
|
# flag msg...
|
||||||
parser_flag = subparsers.add_parser(
|
parser_flag = subparsers.add_parser(
|
||||||
|
@ -811,10 +872,12 @@ class MelCLI():
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
colorama.init()
|
if not os.environ.get("MEL_DEBUG"):
|
||||||
try:
|
|
||||||
CLI = MelCLI()
|
CLI = MelCLI()
|
||||||
except:
|
else:
|
||||||
EXTYPE, VALUE, TB = sys.exc_info()
|
try:
|
||||||
traceback.print_exc()
|
CLI = MelCLI()
|
||||||
pdb.post_mortem(TB)
|
except:
|
||||||
|
EXTYPE, VALUE, TB = sys.exc_info()
|
||||||
|
traceback.print_exc()
|
||||||
|
pdb.post_mortem(TB)
|
||||||
|
|
32
config/scripts/music_remove_dashes
Executable file
32
config/scripts/music_remove_dashes
Executable file
|
@ -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()
|
|
@ -146,6 +146,23 @@ do
|
||||||
|
|
||||||
done <<< "$(find "$dir/" -type f -iname "*.png")"
|
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)
|
# # SVG (requires scour)
|
||||||
# while read image
|
# while read image
|
||||||
# do
|
# do
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
# Normalisation is done at the default of each program,
|
# Normalisation is done at the default of each program,
|
||||||
# which is usually -89.0 dB
|
# 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 logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
|
@ -152,6 +152,7 @@ then
|
||||||
i notmuch # Index mail
|
i notmuch # Index mail
|
||||||
i neomutt || i mutt # CLI mail client
|
i neomutt || i mutt # CLI mail client
|
||||||
i lynx # CLI web browser (for HTML mail)
|
i lynx # CLI web browser (for HTML mail)
|
||||||
|
i tiv # CLI image viewer
|
||||||
if $INSTALL_GUI
|
if $INSTALL_GUI
|
||||||
then
|
then
|
||||||
i thunderbird # GUI mail client (just in case)
|
i thunderbird # GUI mail client (just in case)
|
||||||
|
@ -175,6 +176,7 @@ then
|
||||||
i duperemove # Find and merge dupplicates on BTRFS partitions
|
i duperemove # Find and merge dupplicates on BTRFS partitions
|
||||||
i optipng # Optimize PNG files
|
i optipng # Optimize PNG files
|
||||||
i jpegtran libjpeg-turbo # Compress JPEG files
|
i jpegtran libjpeg-turbo # Compress JPEG files
|
||||||
|
i reflac # Recompress FLAC files
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue