- 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
195 lines
7.0 KiB
Python
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()
|