#!/usr/bin/env python3 """ CLI entry point: re-download mod files that have changed on the server. Use this after you have updated mod files on the Caddy server without changing the modlist structure (same mods, same Steam IDs, new file versions). Detection method: compare local file sizes against server file sizes. A file is considered stale when it is missing locally OR its local size differs from the server-reported size. Use --force to ignore size checks and re-download every file unconditionally. Usage: python update_mods.py # check all groups/mods python update_mods.py --group shared # limit to one group python update_mods.py --mod @ace # limit to one mod folder python update_mods.py --force # re-download everything python update_mods.py --force --group shared # force-update one group """ from __future__ import annotations import argparse import sys from tqdm import tqdm from arma_modlist_tools.compat import fix_console_encoding from arma_modlist_tools.config import load_config from arma_modlist_tools.fetcher import ( build_server_index, find_mod_folder, list_mod_files, list_mod_updates, download_file, make_session, ) fix_console_encoding() def _fmt_bytes(n: int) -> str: for unit in ("B", "KB", "MB", "GB"): if n < 1024: return f"{n:.1f} {unit}" n /= 1024 return f"{n:.1f} TB" def _collect_targets(cfg, group_filter: str | None, mod_filter: str | None) -> list[tuple[str, str, object]]: """ Walk downloads/ and return (group, folder_name, folder_path) for each @Mod folder that passes the group/mod filters. """ targets = [] if not cfg.downloads.is_dir(): return targets for group_dir in sorted(cfg.downloads.iterdir()): if not group_dir.is_dir(): continue if group_filter and group_dir.name != group_filter: continue for mod_dir in sorted(group_dir.iterdir()): if not mod_dir.is_dir() or not mod_dir.name.startswith("@"): continue if mod_filter and mod_dir.name != mod_filter: continue targets.append((group_dir.name, mod_dir.name, mod_dir)) return targets def main() -> None: cfg = load_config() parser = argparse.ArgumentParser( description="Re-download mod files that have changed on the server." ) parser.add_argument("--group", "-g", metavar="GROUP", help="Only check mods in this group folder (e.g. shared)") parser.add_argument("--mod", "-m", metavar="MOD", help="Only check this mod folder name (e.g. @ace)") parser.add_argument("--force", action="store_true", help="Re-download all files regardless of size match") args = parser.parse_args() # ---- Collect local mod folders to check ---- targets = _collect_targets(cfg, args.group, args.mod) if not targets: print("\nNo mod folders found matching the given filters.\n") sys.exit(0) # ---- Build server index ---- print(f"\nBuilding server index...") index = build_server_index(cfg.server_url, cfg.server_auth) print(f" {len(index['by_steam_id'])} mods indexed\n") session = make_session(cfg.server_auth) mode = "force" if args.force else "size-check" print(f" Mode: {mode}") print(f" Checking {len(targets)} mod folder(s)...\n") # Column widths for the summary table COL_MOD = 44 COL_GROUP = 24 total_checked = total_updated = total_bytes = 0 not_on_server = [] for group, folder_name, mod_dir in targets: # Find this mod on the server by name (no steam_id available from local dir) mod_stub = {"name": folder_name, "steam_id": None} folder_url = find_mod_folder(mod_stub, index) if not folder_url: tqdm.write(f" [?] {folder_name:<{COL_MOD}} {group:<{COL_GROUP}} not found on server") not_on_server.append(f"{group}/{folder_name}") continue # Determine which files need downloading if args.force: stale = list_mod_files(folder_url, session) else: stale = list_mod_updates(folder_url, mod_dir, session) all_files = list_mod_files(folder_url, session) if not args.force else stale checked = len(all_files) if not args.force else len(stale) if not stale: print(f" [=] {folder_name:<{COL_MOD}} {group:<{COL_GROUP}} {checked} files up-to-date") total_checked += checked continue # Download stale files mod_bytes = 0 with tqdm( total=len(stale), unit="file", desc=f" {folder_name[-COL_MOD:]:<{COL_MOD}}", position=0, leave=True, dynamic_ncols=True, ) as file_bar: for rel, url, size in stale: dest_file = mod_dir / rel with tqdm( total=size if size else None, unit="B", unit_scale=True, unit_divisor=1024, desc=f" {rel[-40:]:40s}", position=1, leave=False, dynamic_ncols=True, ) as chunk_bar: n = download_file(url, dest_file, session, on_chunk=lambda b: chunk_bar.update(b)) mod_bytes += n file_bar.update(1) total_checked += checked total_updated += len(stale) total_bytes += mod_bytes print(f" [+] {folder_name:<{COL_MOD}} {group:<{COL_GROUP}} " f"{checked} files {len(stale)} updated ({_fmt_bytes(mod_bytes)})") print(f"\n{'='*56}") print(f" Total: {total_checked} files checked, " f"{total_updated} updated, " f"{_fmt_bytes(total_bytes)} downloaded") if not_on_server: print(f" Not found on server ({len(not_on_server)}): {', '.join(not_on_server)}") print(f"{'='*56}\n") if total_updated == 0 and not not_on_server: print(" All mods are up-to-date.\n") if __name__ == "__main__": main()