90 lines
2.8 KiB
Python
90 lines
2.8 KiB
Python
#!/usr/bin/env python3
|
|||
|
|
"""Render rsync's per-file output as a single updating status line.
|
||
|
|
|
||
|
|
Fed the paths rsync reports with --out-format='%n', one per line. Prints one
|
||
|
|
line that rewrites itself, showing how far through the transfer is and which
|
||
|
|
album is currently going across, rather than either scrolling fifty thousand
|
||
|
|
filenames past or -- as rsync does while it builds its file list -- saying
|
||
|
|
nothing at all for several minutes.
|
||
|
|
|
||
|
|
Falls back to periodic plain lines when stderr is not a terminal, so a log does
|
||
|
|
not fill up with carriage returns.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import os
|
||
|
|
import shutil
|
||
|
|
import sys
|
||
|
|
import time
|
||
|
|
|
||
|
|
|
||
|
|
def album_of(path):
|
||
|
|
"""Return "Artist / Album" for a mirror-relative path."""
|
||
|
|
parts = [part for part in path.strip("/").split("/") if part]
|
||
|
|
if len(parts) >= 3:
|
||
|
|
return f"{parts[0]} / {parts[1]}"
|
||
|
|
if len(parts) == 2:
|
||
|
|
return parts[0]
|
||
|
|
return ""
|
||
|
|
|
||
|
|
|
||
|
|
def fit(text, width):
|
||
|
|
"""Trim to the terminal, from the left: the album matters more than the artist."""
|
||
|
|
if width <= 1 or len(text) <= width:
|
||
|
|
return text
|
||
|
|
return "…" + text[-(width - 1) :]
|
||
|
|
|
||
|
|
|
||
|
|
def render(done, total, label, width):
|
||
|
|
if total > 0:
|
||
|
|
share = min(100, done * 100 // total)
|
||
|
|
head = f"[{done:>6,} / {total:<6,} {share:>3}%] "
|
||
|
|
else:
|
||
|
|
head = f"[{done:>6,} files] "
|
||
|
|
return head + fit(label, max(0, width - len(head)))
|
||
|
|
|
||
|
|
|
||
|
|
def main(argv=None, stream=None, out=None):
|
||
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
||
|
|
parser.add_argument("--total", type=int, default=0, help="files expected")
|
||
|
|
parser.add_argument("--interval", type=float, default=0.1, help="seconds between redraws")
|
||
|
|
args = parser.parse_args(argv)
|
||
|
|
|
||
|
|
stream = stream or sys.stdin
|
||
|
|
out = out or sys.stderr
|
||
|
|
interactive = out.isatty()
|
||
|
|
width = shutil.get_terminal_size((100, 24)).columns - 1
|
||
|
|
|
||
|
|
done = 0
|
||
|
|
last_drawn = 0.0
|
||
|
|
label = ""
|
||
|
|
for line in stream:
|
||
|
|
path = line.rstrip("\n")
|
||
|
|
# rsync reports directories too, with a trailing slash. They are not
|
||
|
|
# files and counting them would put the percentage past a hundred.
|
||
|
|
if not path or path.endswith("/"):
|
||
|
|
continue
|
||
|
|
done += 1
|
||
|
|
label = album_of(path) or os.path.basename(path)
|
||
|
|
|
||
|
|
now = time.monotonic()
|
||
|
|
if interactive:
|
||
|
|
if now - last_drawn >= args.interval:
|
||
|
|
out.write("\r\033[2K" + render(done, args.total, label, width))
|
||
|
|
out.flush()
|
||
|
|
last_drawn = now
|
||
|
|
elif now - last_drawn >= 30:
|
||
|
|
out.write(render(done, args.total, label, width) + "\n")
|
||
|
|
out.flush()
|
||
|
|
last_drawn = now
|
||
|
|
|
||
|
|
if interactive:
|
||
|
|
out.write("\r\033[2K")
|
||
|
|
out.write(render(done, args.total, label, width).rstrip() + "\n")
|
||
|
|
out.flush()
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
sys.exit(main())
|