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
193 lines
6.9 KiB
Python
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()
|