From 99cc35e732f8a4153ed881ba93fc67ed16acd0d8 Mon Sep 17 00:00:00 2001
From: Janne Mareike Koschinski <janne@kuschku.de>
Date: Fri, 17 Dec 2021 18:26:42 +0100
Subject: [PATCH] feat: add status output when used in a terminal

---
 audio-renamer.py           | 10 +++++++++
 customio/OtherOutput.py    | 16 ++++++++++++++
 customio/Output.py         | 17 +++++++++++++++
 customio/StatusPrinter.py  | 22 +++++++++++++++++++
 customio/TerminalOutput.py | 44 ++++++++++++++++++++++++++++++++++++++
 customio/streams.py        | 23 ++++++++++++++++++++
 files/FileFinder.py        |  6 ++++++
 files/FileMover.py         |  7 +++---
 models/FileMove.py         |  8 ++++---
 9 files changed, 147 insertions(+), 6 deletions(-)
 create mode 100644 customio/OtherOutput.py
 create mode 100644 customio/Output.py
 create mode 100644 customio/StatusPrinter.py
 create mode 100644 customio/TerminalOutput.py
 create mode 100644 customio/streams.py

diff --git a/audio-renamer.py b/audio-renamer.py
index 548dddd..eda347e 100755
--- a/audio-renamer.py
+++ b/audio-renamer.py
@@ -3,6 +3,8 @@ import argparse
 import os
 from typing import Iterator, List
 
+from customio.StatusPrinter import StatusPrinter
+from customio.streams import stdout
 from files.FileFinder import FileFinder
 from files.FileMover import FileMover
 from models.FileMove import FileMove
@@ -10,22 +12,30 @@ from models.FileMove import FileMove
 
 def process_folders(folders: List[str]) -> Iterator[FileMove]:
     for foldername in folders:
+        stdout().status("Reading folder… {}".format(foldername))
         folder = os.path.abspath(foldername)
         finder = FileFinder(folder)
         mover = FileMover(folder)
+        status = StatusPrinter(stdout(), "Reading Files", finder.count())
         for file in finder.all():
             move = mover.move(file)
+            status.update(os.path.relpath(file, foldername))
             if move is not None:
                 yield move
 
 
 if __name__ == "__main__":
+    stdout().status("Initializing…")
     parser = argparse.ArgumentParser()
     parser.add_argument("folders", nargs="+", help="Folders to run the task on")
     parser.add_argument("--dry-run", help="Simulate renames", action="store_true")
     args = parser.parse_args()
     moves = list(process_folders(args.folders))
+    statusTmp = StatusPrinter(stdout(), "Creating temporary files", len(moves))
     for move in moves:
+        statusTmp.update(move.source)
         move.move_intermediate(args.dry_run)
+    statusFinal = StatusPrinter(stdout(), "Writing files", len(moves))
     for move in moves:
+        statusTmp.update(move.source)
         move.move_target(args.dry_run)
diff --git a/customio/OtherOutput.py b/customio/OtherOutput.py
new file mode 100644
index 0000000..755863f
--- /dev/null
+++ b/customio/OtherOutput.py
@@ -0,0 +1,16 @@
+import io
+
+from customio.Output import Output
+
+
+class OtherOutput(Output):
+    file: io.TextIOBase
+
+    def __init__(self, file: io.TextIOBase):
+        self.file = file
+
+    def status(self, data: str):
+        pass
+
+    def log(self, data: str):
+        print(data, file=self.file)
diff --git a/customio/Output.py b/customio/Output.py
new file mode 100644
index 0000000..9a04064
--- /dev/null
+++ b/customio/Output.py
@@ -0,0 +1,17 @@
+# noinspection PyMethodMayBeStatic
+class Output:
+    def status(self, data: str):
+        """
+        Print a temporary line of data
+        :param data: content to print
+        :return: Nothing
+        """
+        pass
+
+    def log(self, data: str):
+        """
+        Print a line of data
+        :param data: content to print
+        :return: Nothing
+        """
+        pass
diff --git a/customio/StatusPrinter.py b/customio/StatusPrinter.py
new file mode 100644
index 0000000..1444ca2
--- /dev/null
+++ b/customio/StatusPrinter.py
@@ -0,0 +1,22 @@
+from customio import Output
+
+
+class StatusPrinter:
+    task: str
+    index: int
+    total: int
+
+    def __init__(self, output: Output, task: str, total: int):
+        self.output = output
+        self.task = task
+        self.index = 0
+        self.total = total
+
+    def update(self, file: str):
+        data = self.task + '… '
+        if self.total != 0:
+            data += "{} / {} ".format(self.index, self.total)
+        if file != '':
+            data += str(file)
+        self.output.status(data)
+        self.index += 1
diff --git a/customio/TerminalOutput.py b/customio/TerminalOutput.py
new file mode 100644
index 0000000..a73e428
--- /dev/null
+++ b/customio/TerminalOutput.py
@@ -0,0 +1,44 @@
+import io
+import shutil
+
+from urwid import str_util
+
+from customio.Output import Output
+
+
+class TerminalOutput(Output):
+    file: io.TextIOBase
+
+    def __init__(self, file: io.TextIOBase):
+        self.file = file
+
+    def status(self, text: str):
+        print("\x1B[K", end='')
+        print(TerminalOutput.__truncate_terminal(text), end='\r', flush=True, file=self.file)
+
+    def log(self, data: str, error: bool = False):
+        if self.file.isatty():
+            print("\x1B[K", end="", file=self.file)
+            if error:
+                print("\x1B[1;31m", end="", file=self.file)
+        print(data, file=self.file)
+
+    @staticmethod
+    def __truncate_terminal(text: str) -> str:
+        columns = shutil.get_terminal_size((-1, -1)).columns
+        if columns <= 0:
+            return text
+        else:
+            return TerminalOutput.__truncate_width(text, columns)
+
+    @staticmethod
+    def __truncate_width(text, length):
+        result = ""
+        width = 0
+        for char in text:
+            charwidth = str_util.get_width(ord(char))
+            if width + charwidth >= length:
+                break
+            result += char
+            width += charwidth
+        return result
diff --git a/customio/streams.py b/customio/streams.py
new file mode 100644
index 0000000..3145196
--- /dev/null
+++ b/customio/streams.py
@@ -0,0 +1,23 @@
+import io
+import sys
+
+from customio.Output import Output
+from customio.OtherOutput import OtherOutput
+from customio.TerminalOutput import TerminalOutput
+
+
+def get_output(target: io.TextIOBase) -> Output:
+    if target.isatty():
+        return TerminalOutput(target)
+    else:
+        return OtherOutput(target)
+
+
+def stdout():
+    # noinspection PyTypeChecker
+    return get_output(sys.stdout)
+
+
+def stderr():
+    # noinspection PyTypeChecker
+    return get_output(sys.stderr)
diff --git a/files/FileFinder.py b/files/FileFinder.py
index be6f988..fb6f41e 100644
--- a/files/FileFinder.py
+++ b/files/FileFinder.py
@@ -12,3 +12,9 @@ class FileFinder:
         for subdir, dirs, files in os.walk(self.folder):
             for filename in files:
                 yield os.path.join(self.folder, subdir, filename)
+
+    def count(self) -> int:
+        count = 0
+        for subdir, dirs, files in os.walk(self.folder):
+            count += len(files)
+        return count
diff --git a/files/FileMover.py b/files/FileMover.py
index 34a8c60..fb229a5 100644
--- a/files/FileMover.py
+++ b/files/FileMover.py
@@ -2,6 +2,7 @@ import os
 from typing import Optional
 from uuid import uuid4
 
+from customio.streams import stderr
 from extractors.get_extractor import get_extractor
 from files.FileMetaParser import FileMetaParser
 from models.FileMeta import FileMeta
@@ -38,15 +39,15 @@ class FileMover:
     def move(self, name: str) -> Optional[FileMove]:
         extractor = get_extractor(name)
         if extractor is None:
-            print("No Extractor for file: {0}".format(name))
+            stderr().log("No Extractor for file: {0}".format(name))
             return None
         track_meta = extractor.extract_tags()
         if track_meta is None:
-            print("No Metadata for file: {0}".format(name))
+            stderr().log("No Metadata for file: {0}".format(name))
             return None
         file_meta = FileMetaParser(track_meta).get_meta()
         if file_meta is None:
-            print("Metadata for file not valid: {0}".format(name))
+            stderr().log("Metadata for file not valid: {0}".format(name))
             return None
 
         target = self.__get_target_path(name, file_meta)
diff --git a/models/FileMove.py b/models/FileMove.py
index d1a6870..a2f6d2b 100644
--- a/models/FileMove.py
+++ b/models/FileMove.py
@@ -1,5 +1,7 @@
 import os
 
+from customio.streams import stdout
+
 
 class FileMove:
     source: str
@@ -20,9 +22,9 @@ class FileMove:
 
     def move_target(self, dry_run: bool = True):
         if dry_run:
-            print("Moving File:")
-            print("  ← {0}".format(self.source))
-            print("  → {0}".format(self.target))
+            stdout().log("Moving File:")
+            stdout().log("  ← {0}".format(self.source))
+            stdout().log("  → {0}".format(self.target))
         else:
             target_folder = os.path.dirname(self.target)
             if not os.path.exists(target_folder):
-- 
GitLab