#!/usr/bin/env python3 import json import os import sys import unicodedata from typing import Dict, List, Tuple, Any DEFAULT_GAMES_JSON = os.path.join( os.path.dirname(__file__), "steam_library_with_sizes.json" ) DEFAULT_GAMES_ROOT = "/mnt/nextcloud-hdd/games" IGNORE_DIRS = {".", "..", "common", "lost+found"} def normalize(text: str) -> str: """Normalize game / directory names for comparison.""" text = unicodedata.normalize("NFKD", text) text = "".join(ch for ch in text if not unicodedata.combining(ch)) text = text.lower() return "".join(ch for ch in text if ch.isalnum()) def load_games(json_path: str) -> List[Dict[str, Any]]: with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) if not isinstance(data, list): raise ValueError(f"Unexpected JSON structure in {json_path}, expected list") return data def list_game_dirs(root: str) -> List[str]: entries: List[str] = [] for name in os.listdir(root): if name in IGNORE_DIRS or name.startswith("."): continue full = os.path.join(root, name) if os.path.isdir(full): entries.append(name) return sorted(entries) def build_dir_index(dirs: List[str]) -> Dict[str, List[str]]: index: Dict[str, List[str]] = {} for d in dirs: nd = normalize(d) index.setdefault(nd, []).append(d) return index def similar_candidates( norm_name: str, dirs: List[str] ) -> Tuple[float, List[str]]: from difflib import SequenceMatcher best_ratio = 0.0 best: List[str] = [] for d in dirs: nd = normalize(d) ratio = SequenceMatcher(None, norm_name, nd).ratio() if ratio > best_ratio: best_ratio = ratio best = [d] elif ratio == best_ratio and ratio > 0: best.append(d) return best_ratio, best def classify_games( games: List[Dict[str, Any]], server_dirs: List[str] ) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]], Dict[str, int]]: norm_dir_map = build_dir_index(server_dirs) missing: List[Dict[str, Any]] = [] doubtful: List[Dict[str, Any]] = [] for g in games: appid = str(g.get("appid", "")) name = g.get("name", "") norm_name = normalize(name) candidates = norm_dir_map.get(norm_name, []) if len(candidates) == 1: # Confident уникальное соответствие: игра считается присутствующей continue record = { "appid": appid, "name": name, "attempted_match": False, "reason": "", } if len(candidates) > 1: record["attempted_match"] = True record[ "reason" ] = f"Найдено несколько директорий с одинаковым нормализованным именем: {candidates}" missing.append(record) doubtful.append( { "appid": appid, "name": name, "candidates": candidates, "confidence": "низкая уверенность (несколько совпадений по нормализованному имени)", } ) continue # Нет точного совпадения по нормализованному имени: ищем похожие директории, # но не считаем их найденными. best_ratio, best_matches = similar_candidates(norm_name, server_dirs) if best_ratio >= 0.85: record["attempted_match"] = True record["reason"] = ( "Найден(ы) похожие директории, но совпадение не однозначное " f"(коэффициент схожести {best_ratio:.2f}, кандидаты: {best_matches})" ) if best_ratio >= 0.92: conf = "вероятно совпадение" else: conf = "низкая уверенность" doubtful.append( { "appid": appid, "name": name, "candidates": best_matches, "confidence": f"{conf} (similarity={best_ratio:.2f})", } ) else: record[ "reason" ] = "Похожих директорий не найдено (по нормализованному имени)" missing.append(record) stats = { "total_games_in_json": len(games), "total_server_dirs": len(server_dirs), "missing_count": len(missing), "doubtful_count": len(doubtful), } return missing, doubtful, stats def print_markdown( missing: List[Dict[str, Any]], doubtful: List[Dict[str, Any]], stats: Dict[str, int] ) -> None: print("### Статистика") print() print(f"- **Всего игр в JSON**: {stats['total_games_in_json']}") print(f"- **Всего директорий на сервере**: {stats['total_server_dirs']}") print(f"- **Отсутствующие игры**: {stats['missing_count']}") print(f"- **Сомнительные сопоставления**: {stats['doubtful_count']}") print() print("### Игры, отсутствующие на сервере") print() if not missing: print("Все игры из JSON найдены на сервере по строгому сопоставлению.") else: print("| appid | name | попытка сопоставления | комментарий |") print("| --- | --- | --- | --- |") for r in missing: attempted = "да" if r["attempted_match"] else "нет" reason = r.get("reason", "").replace("\n", " ") print(f"| {r['appid']} | {r['name']} | {attempted} | {reason} |") print() print("### Сомнительные/неоднозначные сопоставления") print() if not doubtful: print("Сомнительных сопоставлений не обнаружено.") return print("| appid | name | директории-кандидаты | степень уверенности |") print("| --- | --- | --- | --- |") for d in doubtful: candidates = ", ".join(d.get("candidates") or []) conf = d.get("confidence", "") print(f"| {d['appid']} | {d['name']} | {candidates} | {conf} |") def main(argv: List[str]) -> int: games_json = DEFAULT_GAMES_JSON games_root = DEFAULT_GAMES_ROOT if len(argv) > 1: games_json = argv[1] if len(argv) > 2: games_root = argv[2] if not os.path.isfile(games_json): print(f"ERROR: JSON file not found: {games_json}", file=sys.stderr) return 1 if not os.path.isdir(games_root): print(f"ERROR: Games root not found: {games_root}", file=sys.stderr) return 1 games = load_games(games_json) server_dirs = list_game_dirs(games_root) missing, doubtful, stats = classify_games(games, server_dirs) print_markdown(missing, doubtful, stats) return 0 if __name__ == "__main__": raise SystemExit(main(sys.argv))