113 lines
4.3 KiB
Python
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())
|