"""Réglages du site (site.toml) et des voyages (voyage.toml). Un réglage absent d'un voyage prend la valeur du site, puis la valeur par défaut ci-dessous. Les anciens fichiers (mymaps.txt, moto.txt) sont encore lus quand les nouveaux n'existent pas ; `rtg migrer` écrit les nouveaux. """ import copy import os import re try: import tomllib except ModuleNotFoundError: # Python 3.10 import tomli as tomllib SITE_TOML = "site.toml" VOYAGE_TOML = "voyage.toml" DEFAUTS = { "titre": "Nos Road-Trips", "adresse_publique": "https://roadtrip.ouaibe.fr/", "accueil_nb_recents": 2, "publication": {"mode": "sur_place", "destination": "", "adresse_videos": ""}, "video": { "qualite": "equilibree", # econome | equilibree | haute "encodeur": "processeur", # processeur | materiel "preset": "medium", "fps_max": 60, "jobs": 0, # 0 = automatique "threads": 0, }, "photos": {"grand_cote": 1600, "qualite": 80, "positions_deduites": True}, "journees": {"fuseau": "Europe/Paris", "coupure_heure": 4}, "carte_globale": {"mymaps": []}, # Adresse de l'interface (rtg interface --proxy) : bouton « Connexion » # sur l'accueil du site. Vide = pas de bouton. "administration": {"adresse": ""}, # Propres à un voyage "rubrique": "", "moto": "", "sources": {"traces": ["routes"], "videos": ["rushs"], "photos": ["photos"]}, "exclusions": {"fichiers": []}, "dates_traces": {}, # trace sans heures → jour imposé (AAAA-MM-JJ) "positions": {}, # média placé à la main → [lat, lon] "corrections": {"masques": []}, # zones retirées des traces : [lat, lon, rayon_m] "confidentialite": {"zones": []}, # (site) zones retirées de tous les voyages } # Qualité « humaine » → paramètres d'encodeur. « equilibree » = réglages # historiques de compress.sh (CRF 26 HEVC / 25 H.264). QUALITES = { "econome": {"crf_hevc": 28, "crf_h264": 27, "vtq_hd": 55, "vtq_sd": 53}, "equilibree": {"crf_hevc": 26, "crf_h264": 25, "vtq_hd": 60, "vtq_sd": 58}, "haute": {"crf_hevc": 23, "crf_h264": 22, "vtq_hd": 68, "vtq_sd": 65}, } def fusion(base, ajout): """Fusion récursive : les valeurs de `ajout` l'emportent.""" r = copy.deepcopy(base) for k, v in (ajout or {}).items(): if isinstance(v, dict) and isinstance(r.get(k), dict): r[k] = fusion(r[k], v) else: r[k] = copy.deepcopy(v) return r def lire_toml(chemin): if not os.path.exists(chemin): return {} with open(chemin, "rb") as f: return tomllib.load(f) def trouver_racine(depart="."): """Premier dossier, en remontant depuis `depart`, qui contient site.toml ; à défaut `depart` lui-même.""" d = os.path.abspath(depart) while True: if os.path.exists(os.path.join(d, SITE_TOML)): return d parent = os.path.dirname(d) if parent == d: return os.path.abspath(depart) d = parent def _mymaps_ancien(racine): f = os.path.join(racine, "mymaps.txt") if not os.path.exists(f): return [] with open(f, encoding="utf-8") as fh: return [l.strip() for l in fh if l.strip() and not l.strip().startswith("#")] def reglages_site(racine): brut = lire_toml(os.path.join(racine, SITE_TOML)) cfg = fusion(DEFAUTS, brut) if not brut.get("carte_globale", {}).get("mymaps"): cfg["carte_globale"]["mymaps"] = _mymaps_ancien(racine) return cfg def reglages_voyage(dossier, site=None): """Réglages effectifs d'un voyage : défauts ← site ← voyage.toml (← moto.txt).""" brut = lire_toml(os.path.join(dossier, VOYAGE_TOML)) cfg = fusion(site if site is not None else DEFAUTS, brut) if not brut.get("moto"): f = os.path.join(dossier, "moto.txt") if os.path.exists(f): with open(f, encoding="utf-8") as fh: cfg["moto"] = fh.readline().strip() if not cfg.get("titre") or "titre" not in brut: cfg["titre"] = os.path.basename(dossier.rstrip("/")).replace("-", " ").replace("_", " ") return cfg def parametres_video(cfg): """Paramètres d'encodage effectifs (dictionnaire plat).""" v = cfg["video"] q = QUALITES.get(v.get("qualite", "equilibree"), QUALITES["equilibree"]) # crf_hevc / crf_h264 dans [video] l'emportent sur la qualité choisie (essais). q = dict(q, **{k: int(v[k]) for k in ("crf_hevc", "crf_h264") if k in v}) return dict(q, encodeur="vt" if v.get("encodeur") == "materiel" else "cpu", preset=v.get("preset", "medium"), fps_max=int(v.get("fps_max", 60)), jobs=int(v.get("jobs", 0)), threads=int(v.get("threads", 0))) # --- Écriture (rtg migrer, interface) ----------------------------------------- def _toml_valeur(v): if isinstance(v, bool): return "true" if v else "false" if isinstance(v, (int, float)): return str(v) if isinstance(v, list): return "[" + ", ".join(_toml_valeur(x) for x in v) + "]" s = str(v).replace("\\", "\\\\").replace('"', '\\"') return f'"{s}"' def _toml_cle(k): return k if re.fullmatch(r"[A-Za-z0-9_-]+", k) else _toml_valeur(k) def ecrire_toml(chemin, donnees, entete=""): """Écrit un TOML simple (valeurs, listes, une profondeur de tables).""" lignes = [f"# {l}" for l in entete.splitlines()] + ([""] if entete else []) for k, v in donnees.items(): if not isinstance(v, dict): lignes.append(f"{_toml_cle(k)} = {_toml_valeur(v)}") for k, v in donnees.items(): if isinstance(v, dict): lignes += ["", f"[{_toml_cle(k)}]"] + [f"{_toml_cle(kk)} = {_toml_valeur(vv)}" for kk, vv in v.items()] with open(chemin, "w", encoding="utf-8") as f: f.write("\n".join(lignes) + "\n") def modifier_toml(chemin, changements, entete=""): """Applique `changements` à un fichier TOML existant (ou nouveau) et le réécrit. Une valeur None retire la clé ; une table est fusionnée sur un niveau. Le commentaire d'en-tête est conservé, les autres sont perdus.""" donnees = lire_toml(chemin) if os.path.exists(chemin): with open(chemin, encoding="utf-8") as f: tete = [] for l in f: if not l.startswith("#"): break tete.append(l[1:].strip()) entete = "\n".join(tete) or entete for k, v in changements.items(): if v is None: donnees.pop(k, None) elif isinstance(v, dict): table = dict(donnees.get(k, {})) for kk, vv in v.items(): if vv is None: table.pop(kk, None) else: table[kk] = vv if table: donnees[k] = table else: donnees.pop(k, None) else: donnees[k] = v ecrire_toml(chemin, donnees, entete) return donnees def nom_lisible(nom): return re.sub(r"[-_]+", " ", nom).strip()