#!/usr/bin/env python3 """Recupere la liste des apps installees sur le serveur YunoHost et ajoute celles qui manquent dans src/config.json (dans "ungrouped"), sans toucher a celles deja placees dans un groupe, dans "pinned" ou deja presentes. Usage: python3 scripts/sync-apps.py [utilisateur@hote] [domaine] Toute la configuration vient de .env (SYNC_SSH_USER, SYNC_SSH_HOST, PORTAL_DOMAIN, REMOTE_USER) ; les arguments en ligne de commande, s'ils sont fournis, ont priorite sur .env. """ import json import subprocess import sys from pathlib import Path SCRIPT_DIR = Path(__file__).resolve().parent PROJECT_ROOT = SCRIPT_DIR.parent CONFIG_PATH = PROJECT_ROOT / "src" / "config.json" ENV_PATH = PROJECT_ROOT / ".env" def load_env(): env = {} if not ENV_PATH.exists(): return env for line in ENV_PATH.read_text(encoding="utf-8").splitlines(): line = line.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) env[key.strip()] = value.strip() return env ENV = load_env() SYNC_SSH_USER = ENV.get("SYNC_SSH_USER", "root") SYNC_SSH_HOST = ENV.get("SYNC_SSH_HOST") or ENV.get("REMOTE_HOST") PORTAL_DOMAIN = ENV.get("PORTAL_DOMAIN") or ENV.get("REMOTE_HOST") SELF_APP_ID = ENV.get("REMOTE_USER", "") # cette page elle-meme, jamais ajoutee if len(sys.argv) > 1: SSH_TARGET = sys.argv[1] elif SYNC_SSH_HOST: SSH_TARGET = f"{SYNC_SSH_USER}@{SYNC_SSH_HOST}" else: SSH_TARGET = None DOMAIN = sys.argv[2] if len(sys.argv) > 2 else PORTAL_DOMAIN if not SSH_TARGET or not DOMAIN: print( "Configuration manquante : renseigne SYNC_SSH_HOST (ou REMOTE_HOST) " "et PORTAL_DOMAIN dans .env (lance 'make config' si .env n'existe pas " "encore), ou passe-les en argument :\n" " python3 scripts/sync-apps.py utilisateur@hote domaine", file=sys.stderr, ) sys.exit(1) PALETTE = [ "#1db9c3", "#ff5a1f", "#2094f3", "#ffc230", "#00a651", "#e6721a", "#b71c1c", "#f39c12", "#0d3b66", "#3b8a8c", "#568d34", "#609926", "#d24939", "#0066cc", "#8e44ad", "#16a085", "#c0392b", "#2c3e50", ] def color_of(name): h = 0 for ch in name: h = (h * 31 + ord(ch)) & 0xFFFFFFFF return PALETTE[h % len(PALETTE)] def initials_of(name): words = name.strip().split() if len(words) == 1: return words[0][:2].upper() return (words[0][0] + words[1][0]).upper() def fetch_remote_apps(): cmd = ["ssh", "-o", "BatchMode=yes", SSH_TARGET, f"cat /etc/yunohost/portal/{DOMAIN}.json"] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: print(f"Erreur SSH: {result.stderr}", file=sys.stderr) sys.exit(1) return json.loads(result.stdout) def normalize_url(url): return url.replace("https://", "").replace("http://", "").rstrip("/").lower() def known_urls(config): urls = set() for app in config.get("pinned", []): urls.add(normalize_url(app["url"])) for group in config.get("groups", []): for app in group["apps"]: urls.add(normalize_url(app["url"])) for app in config.get("ungrouped", []): urls.add(normalize_url(app["url"])) return urls def main(): with open(CONFIG_PATH, encoding="utf-8") as f: config = json.load(f) remote = fetch_remote_apps() remote_apps = remote.get("apps", {}) existing_urls = known_urls(config) added = [] for perm_key, info in sorted(remote_apps.items()): if not perm_key.endswith(".main"): continue app_id = perm_key[: -len(".main")] if app_id == SELF_APP_ID: continue url = info["url"] if not url.startswith("http"): url = "https://" + url if not url.endswith("/"): url += "/" if normalize_url(url) in existing_urls: continue name = info.get("label", app_id) app = { "id": app_id, "name": name, "url": url, "color": color_of(name), "initials": initials_of(name), } logo = info.get("logo") if logo: app["icon"] = f"https://{DOMAIN}{logo}" config.setdefault("ungrouped", []).append(app) added.append(name) if not added: print("Rien a ajouter, config.json est deja a jour.") return with open(CONFIG_PATH, "w", encoding="utf-8") as f: json.dump(config, f, ensure_ascii=False, indent=2) f.write("\n") print(f"{len(added)} application(s) ajoutee(s) dans \"ungrouped\" :") for name in added: print(f" - {name}") if __name__ == "__main__": main()