Files
2026-09-06 22:30:13 -04:00

113 lines
4.3 KiB
Python

"""Scan, match, and report on PSP discs. Dry-run by default.
Usage::
python3 -m app.scan --roots /mnt/user/psp/ISO
python3 -m app.scan --roots /mnt/user/psp/ISO --apply # actually rename
"""
import argparse
import json
import logging
import sys
from pathlib import Path
from . import config, pipeline, scanner
from .db import init_db
def _roots_from_arg(value):
if value:
raw = value
else:
raw = ",".join(map(str, config.GAMES_ROOTS + config.VIDEO_ROOTS))
return [Path(p) for p in raw.split(",") if p.strip()]
def main(argv=None):
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-7s %(message)s", stream=sys.stderr)
ap = argparse.ArgumentParser(description="Scan PSP .iso discs and match against the catalog")
ap.add_argument("--roots", default=None, help="comma-separated directories to scan")
ap.add_argument("--db", default=None, help="SQLite DB path")
ap.add_argument("--apply", action="store_true", help="apply renames (default: dry-run)")
ap.add_argument("--json", action="store_true", help="output JSON")
ap.add_argument("--workers", type=int, default=config.SCAN_WORKERS, help="parallel hashing workers")
ap.add_argument("--quick", action="store_true", help="skip full-file hashing (serial/title match only)")
args = ap.parse_args(argv)
roots = _roots_from_arg(args.roots)
db_path = Path(args.db) if args.db else config.DB_PATH
conn = init_db(db_path)
try:
results = []
for record in scanner.scan(roots, conn=conn, workers=args.workers, hash_files=not args.quick):
ident = pipeline.identify(conn, record)
results.append({**record, **ident})
if not args.json:
if ident["method"] == "unknown":
print(f"UNKNOWN {record['name']} serial={record['serial']} error={record['error']}", flush=True)
else:
print(f"MATCH[{ident['method']:14s}] {record['name']} -> {ident['canonical']}", flush=True)
totals = {
row["system"]: row["c"]
for row in conn.execute(
"SELECT d.system, COUNT(*) AS c FROM releases r JOIN dats d ON d.id = r.dat_id GROUP BY d.system"
)
}
on_disk = {}
for r in results:
if r["release"]:
on_disk.setdefault(r["release"]["system"], set()).add(r["release"]["id"])
if args.json:
print(json.dumps(
{
"catalog_totals": totals,
"on_disk": {s: len(ids) for s, ids in on_disk.items()},
"missing": {s: totals.get(s, 0) - len(ids) for s, ids in on_disk.items()},
"discs": [
{k: r.get(k) for k in ("name", "size", "serial", "title", "method", "canonical", "error")}
for r in results
],
},
indent=2,
))
return 0
matched = [r for r in results if r["method"] != "unknown"]
unknown = [r for r in results if r["method"] == "unknown"]
print(f"\nScanned: {len(results)} Matched: {len(matched)} Unknown: {len(unknown)}", flush=True)
for system in sorted(totals):
have = len(on_disk.get(system, set()))
total = totals.get(system, 0)
print(f"Catalog[{system}]: on-disk {have}/{total} missing {total - have}")
renames = [
r for r in results
if r["method"] != "unknown" and r["canonical"]
and Path(r["path"]).name != r["canonical"]
]
if renames:
print(f"\nRename plan ({len(renames)} file(s)):")
for r in renames:
print(f" {Path(r['path']).name}\n -> {r['canonical']}")
if args.apply:
for r in renames:
src = Path(r["path"])
dst = src.with_name(r["canonical"])
if src.exists() and not dst.exists():
scanner.rename_and_recache(conn, src, dst)
print(f" renamed -> {dst.name}")
else:
print(" (dry-run; pass --apply to rename)")
conn.commit()
finally:
conn.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())