Files
music-mirror/tools/rsync_progress.py
T

90 lines
2.8 KiB
Python
Raw Normal View History

#!/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())