Files
RoadTripsGenerator/rtg/config.py
T
FabriceandClaude Opus 5.5 4366db94ed Administration depuis le site : bouton Connexion sur l'accueil
site.toml [administration] adresse : l'accueil affiche un bouton
« Connexion » vers l'interface. Celle-ci peut tourner derrière le serveur
web du site (rtg interface --proxy --prefixe /admin) : adresses
préfixées, mot de passe obligatoire, visiteur identifié par
X-Forwarded-For, cookie Secure limité au préfixe quand le site est en
HTTPS. Exemples nginx et Apache dans le README. Le bouton Arrêter ne
remplace plus le bloc de la tâche en même temps que la relève.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mf2kATMtqVABHSYdxJMnim
2026-10-09 14:49:47 +00:00

193 lines
6.9 KiB
Python

"""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"])
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()