Add --import-qr to run the QR->CSV step before importing
Wrap the QR decode/append step behind --import-qr on the importer so a single command runs the whole pipeline: decode QR images, append only new credentials to the CSV (deduped on otpauth://, existing entries silently skipped), then validate and import. QR folder defaults to the CSV's folder; override with --qr-images. Refactor qr_to_sheet into reusable collect_candidates / apply_new_candidates / run_qr_import.
This commit is contained in:
@@ -116,30 +116,18 @@ def scan_images(image_dir):
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# Orchestration (reused by this CLI and by apex_otp_import --import-qr)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
DEFAULT_CSV = "otp-keys.csv"
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
p = argparse.ArgumentParser(
|
||||
description="Decode OTP QR screenshots and append new credentials to the "
|
||||
"canonical CSV (deduplicated on the otpauth:// data).")
|
||||
p.add_argument("--images", default=".", help="directory of QR images")
|
||||
p.add_argument("--csv", default=DEFAULT_CSV, help=f"CSV path (default: {DEFAULT_CSV})")
|
||||
p.add_argument("--dry-run", action="store_true", help="report without writing")
|
||||
args = p.parse_args(argv)
|
||||
|
||||
images = scan_images(args.images)
|
||||
if not images:
|
||||
print(f"No QR images found in {args.images}")
|
||||
return 1
|
||||
|
||||
candidates = [] # (otpauth_uri, source_file)
|
||||
undecodable = []
|
||||
unsupported = []
|
||||
for path in images:
|
||||
def collect_candidates(image_dir):
|
||||
"""Decode every QR image in a directory. Returns
|
||||
(candidates, undecodable, unsupported) where candidates is a list of
|
||||
(otpauth_uri, source_file)."""
|
||||
candidates, undecodable, unsupported = [], [], []
|
||||
for path in scan_images(image_dir):
|
||||
name = os.path.basename(path)
|
||||
data = decode_qr_image(path)
|
||||
if not data:
|
||||
@@ -150,29 +138,65 @@ def main(argv=None):
|
||||
unsupported.append(name)
|
||||
else:
|
||||
undecodable.append(name)
|
||||
return candidates, undecodable, unsupported
|
||||
|
||||
keys = existing_keys(read_csv_rows(args.csv))
|
||||
|
||||
def apply_new_candidates(candidates, csv_path, dry_run=False):
|
||||
"""Append only candidates whose otpauth is not already in the CSV. Existing
|
||||
rows are never modified; duplicates are silently skipped (counted only)."""
|
||||
keys = existing_keys(read_csv_rows(csv_path))
|
||||
new_rows, dups = dedup_new(candidates, keys)
|
||||
if new_rows and not dry_run:
|
||||
append_csv(csv_path, new_rows)
|
||||
return {"scanned": len(candidates), "added": len(new_rows),
|
||||
"duplicates": dups, "new_rows": new_rows}
|
||||
|
||||
print(f"Scanned {len(images)} image(s): {len(candidates)} otpauth QR(s) decoded.")
|
||||
print(f" {len(new_rows)} new, {dups} duplicate(s) skipped.")
|
||||
if unsupported:
|
||||
print(f" {len(unsupported)} otpauth-migration:// (unsupported): "
|
||||
+ ", ".join(unsupported))
|
||||
if undecodable:
|
||||
print(f" {len(undecodable)} could not be decoded (add manually): "
|
||||
+ ", ".join(undecodable))
|
||||
|
||||
if args.dry_run:
|
||||
for r in new_rows:
|
||||
def run_qr_import(image_dir, csv_path, dry_run=False):
|
||||
"""Decode a folder of QR images and append new credentials to the CSV."""
|
||||
candidates, undecodable, unsupported = collect_candidates(image_dir)
|
||||
summary = apply_new_candidates(candidates, csv_path, dry_run)
|
||||
summary["undecodable"] = undecodable
|
||||
summary["unsupported"] = unsupported
|
||||
summary["images"] = len(candidates) + len(undecodable) + len(unsupported)
|
||||
return summary
|
||||
|
||||
|
||||
def print_qr_summary(summary, csv_path, dry_run=False):
|
||||
print(f"Scanned {summary['images']} image(s): "
|
||||
f"{summary['scanned']} otpauth QR(s) decoded.")
|
||||
print(f" {summary['added']} new, {summary['duplicates']} duplicate(s) skipped.")
|
||||
if summary["unsupported"]:
|
||||
print(" otpauth-migration:// (unsupported): " + ", ".join(summary["unsupported"]))
|
||||
if summary["undecodable"]:
|
||||
print(" could not be decoded (add manually): " + ", ".join(summary["undecodable"]))
|
||||
if dry_run:
|
||||
for r in summary["new_rows"]:
|
||||
print(f" + {r['issuer']}:{r['account']} ({r['source_file']})")
|
||||
return 0
|
||||
|
||||
if new_rows:
|
||||
append_csv(args.csv, new_rows)
|
||||
print(f"Appended {len(new_rows)} row(s) to {args.csv}.")
|
||||
elif summary["added"]:
|
||||
print(f"Appended {summary['added']} row(s) to {csv_path}.")
|
||||
else:
|
||||
print("Nothing to append.")
|
||||
print("Nothing new to append.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main(argv=None):
|
||||
p = argparse.ArgumentParser(
|
||||
description="Decode OTP QR screenshots and append new credentials to the "
|
||||
"canonical CSV (deduplicated on the otpauth:// data).")
|
||||
p.add_argument("--images", default=".", help="directory of QR images")
|
||||
p.add_argument("--csv", default=DEFAULT_CSV, help=f"CSV path (default: {DEFAULT_CSV})")
|
||||
p.add_argument("--dry-run", action="store_true", help="report without writing")
|
||||
args = p.parse_args(argv)
|
||||
|
||||
summary = run_qr_import(args.images, args.csv, dry_run=args.dry_run)
|
||||
if summary["images"] == 0:
|
||||
print(f"No QR images found in {args.images}")
|
||||
return 1
|
||||
print_qr_summary(summary, args.csv, args.dry_run)
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user