mirror of
https://github.com/seanbetts/steam-hardware-watch.git
synced 2026-10-06 05:02:52 +02:00
342 lines
11 KiB
Python
Executable File
342 lines
11 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import argparse
|
|
import json
|
|
import os
|
|
import urllib.error
|
|
import urllib.request
|
|
from pathlib import Path
|
|
from urllib.parse import urlparse
|
|
|
|
VISUAL_EXTS = {
|
|
".png",
|
|
".jpg",
|
|
".jpeg",
|
|
".webp",
|
|
".avif",
|
|
".svg",
|
|
".gif",
|
|
".mp4",
|
|
".webm",
|
|
".mov",
|
|
".m4v",
|
|
}
|
|
|
|
DEFAULT_USER_AGENT = (
|
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
|
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
"Chrome/123.0.0.0 Safari/537.36"
|
|
)
|
|
|
|
|
|
def is_visual_url(url: str) -> bool:
|
|
path = urlparse(url).path.lower()
|
|
return any(path.endswith(ext) for ext in VISUAL_EXTS)
|
|
|
|
|
|
def walk_json(value):
|
|
if isinstance(value, dict):
|
|
yield value
|
|
for child in value.values():
|
|
yield from walk_json(child)
|
|
elif isinstance(value, list):
|
|
for item in value:
|
|
yield from walk_json(item)
|
|
|
|
|
|
def collect_from_json(path: Path):
|
|
if path.name in {"media-spec-search.json", "media-manual-search.json", "media-video.json"}:
|
|
return []
|
|
|
|
try:
|
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
except Exception:
|
|
return []
|
|
|
|
hits = []
|
|
for obj in walk_json(data):
|
|
if not isinstance(obj, dict):
|
|
continue
|
|
url = obj.get("source_url")
|
|
mime = obj.get("mime_type", "")
|
|
if isinstance(url, str) and url.startswith("http"):
|
|
if mime.startswith("image/") or mime.startswith("video/") or is_visual_url(url):
|
|
hits.append((url, mime, str(path)))
|
|
return hits
|
|
|
|
|
|
def collect_from_seed_reports(base_dir: Path, current_run_dir: Path):
|
|
if not base_dir.exists():
|
|
return []
|
|
|
|
hits = []
|
|
patterns = (
|
|
("discovered-visual-assets.tsv", 0, 2),
|
|
("retrieved-visual-assets.tsv", 0, 2),
|
|
("blocked-visual-assets.tsv", None, 4),
|
|
("downloaded-visual-assets.tsv", 0, 2),
|
|
)
|
|
|
|
for report_name, mime_index, url_index in patterns:
|
|
for report in sorted(base_dir.glob(f"????-??-??/reports/{report_name}")):
|
|
run_dir = report.parent.parent
|
|
if run_dir == current_run_dir:
|
|
continue
|
|
for raw_line in report.read_text(encoding="utf-8", errors="ignore").splitlines():
|
|
line = raw_line.strip()
|
|
if not line:
|
|
continue
|
|
parts = line.split("\t")
|
|
if len(parts) <= url_index:
|
|
continue
|
|
url = parts[url_index]
|
|
if not isinstance(url, str) or not url.startswith("http") or not is_visual_url(url):
|
|
continue
|
|
mime = parts[mime_index] if mime_index is not None and len(parts) > mime_index else ""
|
|
hits.append((url, mime, f"seed:{run_dir.name}:{report_name}"))
|
|
return hits
|
|
|
|
|
|
def destination_for(base_dir: Path, url: str) -> Path:
|
|
parsed = urlparse(url)
|
|
rel = (parsed.netloc + parsed.path).lstrip("/")
|
|
return base_dir / rel
|
|
|
|
|
|
def is_protected_komodo_asset(url: str) -> bool:
|
|
parsed = urlparse(url)
|
|
host = parsed.netloc.lower()
|
|
path = parsed.path.lower()
|
|
if "komodostation.com" not in host:
|
|
return False
|
|
return path.endswith((".mp4", ".webm", ".mov", ".m4v", ".avif"))
|
|
|
|
|
|
def detect_format(data: bytes) -> str:
|
|
head = data[:512]
|
|
lower = head.lower()
|
|
if head.startswith(b"\x89PNG\r\n\x1a\n"):
|
|
return "png"
|
|
if head.startswith(b"\xff\xd8\xff"):
|
|
return "jpeg"
|
|
if head.startswith((b"GIF87a", b"GIF89a")):
|
|
return "gif"
|
|
if head.startswith(b"RIFF") and b"WEBP" in head[:16]:
|
|
return "webp"
|
|
if head.startswith(b"\x1a\x45\xdf\xa3"):
|
|
return "webm"
|
|
if b"ftyp" in head[:32]:
|
|
if b"avif" in head[:32] or b"avis" in head[:32]:
|
|
return "avif"
|
|
return "mp4"
|
|
if b"<svg" in lower:
|
|
return "svg"
|
|
if lower.startswith(b"<!doctype html") or lower.startswith(b"<html"):
|
|
return "html"
|
|
return "unknown"
|
|
|
|
|
|
def expected_family(url: str) -> str:
|
|
path = urlparse(url).path.lower()
|
|
if path.endswith(".png"):
|
|
return "png"
|
|
if path.endswith((".jpg", ".jpeg")):
|
|
return "jpeg"
|
|
if path.endswith(".gif"):
|
|
return "gif"
|
|
if path.endswith(".webp"):
|
|
return "webp"
|
|
if path.endswith(".avif"):
|
|
return "avif"
|
|
if path.endswith(".svg"):
|
|
return "svg"
|
|
if path.endswith(".webm"):
|
|
return "webm"
|
|
if path.endswith((".mp4", ".mov", ".m4v")):
|
|
return "mp4"
|
|
return "unknown"
|
|
|
|
|
|
def is_valid_download(url: str, content_type: str, data: bytes):
|
|
detected = detect_format(data)
|
|
expected = expected_family(url)
|
|
ctype = (content_type or "").lower()
|
|
|
|
if detected == "html":
|
|
return False, detected, expected
|
|
if ctype.startswith("text/html"):
|
|
return False, detected, expected
|
|
if expected != "unknown" and detected != expected:
|
|
return False, detected, expected
|
|
if detected == "unknown":
|
|
return False, detected, expected
|
|
return True, detected, expected
|
|
|
|
|
|
def download(url: str, dest: Path):
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
req = urllib.request.Request(
|
|
url,
|
|
headers={"User-Agent": os.environ.get("KOMODO_USER_AGENT", DEFAULT_USER_AGENT)},
|
|
)
|
|
with urllib.request.urlopen(req, timeout=15) as response:
|
|
data = response.read()
|
|
status = getattr(response, "status", 200)
|
|
content_type = response.headers.get_content_type()
|
|
ok, detected, expected = is_valid_download(url, content_type, data)
|
|
if not ok:
|
|
return {
|
|
"ok": False,
|
|
"status": str(status),
|
|
"content_type": content_type,
|
|
"detected": detected,
|
|
"expected": expected,
|
|
}
|
|
dest.write_bytes(data)
|
|
return {
|
|
"ok": True,
|
|
"status": str(status),
|
|
"content_type": content_type,
|
|
"detected": detected,
|
|
"expected": expected,
|
|
}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Save discovered visual assets from a run directory.")
|
|
parser.add_argument("--run-dir", required=True)
|
|
parser.add_argument("--base-dir", help="Optional base directory containing prior dated runs to seed discovery from")
|
|
args = parser.parse_args()
|
|
|
|
run_dir = Path(args.run_dir)
|
|
base_dir = Path(args.base_dir) if args.base_dir else run_dir.parent
|
|
api_dir = run_dir / "api"
|
|
asset_dir = run_dir / "assets" / "discovered"
|
|
report_dir = run_dir / "reports"
|
|
report_dir.mkdir(parents=True, exist_ok=True)
|
|
asset_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
hits = []
|
|
for path in (api_dir / "komodo").rglob("*.json"):
|
|
hits.extend(collect_from_json(path))
|
|
hits.extend(collect_from_seed_reports(base_dir, run_dir))
|
|
|
|
unique = {}
|
|
for url, mime, source in hits:
|
|
unique.setdefault(url, {"mime": mime, "sources": set()})
|
|
if mime and not unique[url]["mime"]:
|
|
unique[url]["mime"] = mime
|
|
unique[url]["sources"].add(source)
|
|
|
|
discovered_rows = []
|
|
retrieved_rows = []
|
|
error_rows = []
|
|
for url in sorted(unique):
|
|
discovered_rows.append(
|
|
"\t".join(
|
|
[
|
|
unique[url]["mime"],
|
|
expected_family(url),
|
|
url,
|
|
",".join(sorted(unique[url]["sources"])),
|
|
]
|
|
)
|
|
)
|
|
dest = destination_for(asset_dir, url)
|
|
if dest.exists():
|
|
data = dest.read_bytes()[:512]
|
|
ok, detected, expected = is_valid_download(url, unique[url]["mime"], data)
|
|
if not ok:
|
|
dest.unlink(missing_ok=True)
|
|
|
|
result = None
|
|
if not dest.exists():
|
|
if is_protected_komodo_asset(url):
|
|
result = {
|
|
"ok": False,
|
|
"status": "session_required",
|
|
"content_type": unique[url]["mime"],
|
|
"detected": "protected_komodo_asset",
|
|
"expected": expected_family(url),
|
|
}
|
|
else:
|
|
try:
|
|
result = download(url, dest)
|
|
except urllib.error.HTTPError as err:
|
|
result = {
|
|
"ok": False,
|
|
"status": str(err.code),
|
|
"content_type": getattr(err, "headers", {}).get_content_type() if getattr(err, "headers", None) else "",
|
|
"detected": "http_error",
|
|
"expected": expected_family(url),
|
|
}
|
|
except Exception as err:
|
|
result = {
|
|
"ok": False,
|
|
"status": "error",
|
|
"content_type": "",
|
|
"detected": type(err).__name__,
|
|
"expected": expected_family(url),
|
|
}
|
|
else:
|
|
result = {
|
|
"ok": True,
|
|
"status": "cached",
|
|
"content_type": unique[url]["mime"],
|
|
"detected": detect_format(dest.read_bytes()[:512]),
|
|
"expected": expected_family(url),
|
|
}
|
|
|
|
if result["ok"]:
|
|
retrieved_rows.append(
|
|
"\t".join(
|
|
[
|
|
unique[url]["mime"],
|
|
result["detected"],
|
|
url,
|
|
str(dest),
|
|
",".join(sorted(unique[url]["sources"])),
|
|
]
|
|
)
|
|
)
|
|
else:
|
|
dest.unlink(missing_ok=True)
|
|
error_rows.append(
|
|
"\t".join(
|
|
[
|
|
result["status"],
|
|
result["content_type"],
|
|
result["detected"],
|
|
result["expected"],
|
|
url,
|
|
",".join(sorted(unique[url]["sources"])),
|
|
]
|
|
)
|
|
)
|
|
|
|
(report_dir / "discovered-visual-assets.tsv").write_text(
|
|
"\n".join(discovered_rows) + ("\n" if discovered_rows else ""),
|
|
encoding="utf-8",
|
|
)
|
|
(report_dir / "retrieved-visual-assets.tsv").write_text(
|
|
"\n".join(retrieved_rows) + ("\n" if retrieved_rows else ""),
|
|
encoding="utf-8",
|
|
)
|
|
(report_dir / "blocked-visual-assets.tsv").write_text(
|
|
"\n".join(error_rows) + ("\n" if error_rows else ""),
|
|
encoding="utf-8",
|
|
)
|
|
manual_urls = sorted({row.split("\t")[4] for row in error_rows if len(row.split("\t")) > 4})
|
|
(report_dir / "manual-asset-urls.txt").write_text(
|
|
"\n".join(manual_urls) + ("\n" if manual_urls else ""),
|
|
encoding="utf-8",
|
|
)
|
|
# Backward-compatible alias for earlier script versions.
|
|
(report_dir / "downloaded-visual-assets.tsv").write_text(
|
|
"\n".join(retrieved_rows) + ("\n" if retrieved_rows else ""),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|