Files
FabriceandClaude Opus 5.5 ada3d76f50 Administration native sur le Mac, réencodage sélectif et aide intégrée
- admin.sh : RTG lancé directement sur le Mac (plus de conteneur), nginx
  joint l'interface par host.docker.internal:8765 ; variante conteneur
  conservée (admin-docker.sh, docker-compose-conteneur.yml)
- Compression : réglages lus dans les fichiers (CRF, codec), refaire
  seulement le 1080p ou le 720p, seulement si les réglages ont changé ;
  réglages vidéo propres à chaque voyage ; « Compression actuelle » affichée
- Plus d'arrêt automatique après 2 h d'inactivité (--arret-inactif, 0 par défaut) ;
  arrêt propre qui attend la fin des encodages en cours
- Aide contextuelle dans l'interface (rtg/interface/aide) ; README centré
  sur l'installation et la mise en place
- Cœurs attribués au conteneur détectés (cgroup) et signalés

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mf2kATMtqVABHSYdxJMnim
2026-10-19 05:32:05 +00:00

195 lines
7.0 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"])
# 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()