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
This commit is contained in:
1 parent
b54ae69b19
commit
ada3d76f50
38 files changed
+1067
-264
No files matched your search
+103
-11
@@ -36,8 +36,8 @@ from urllib.parse import parse_qs, quote, unquote, urlsplit
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
|
||||
from rtg import __version__, acces, accueil, corrections, site, sources, video
|
||||
from rtg.config import (QUALITES, SITE_TOML, VOYAGE_TOML, modifier_toml, parametres_video,
|
||||
reglages_site, reglages_voyage)
|
||||
from rtg.config import (QUALITES, SITE_TOML, VOYAGE_TOML, lire_toml, modifier_toml,
|
||||
parametres_video, reglages_site, reglages_voyage)
|
||||
from rtg.interface import taches
|
||||
from rtg.interface.securite import Gardien
|
||||
from rtg.outils import EXT_PHOTOS, EXT_VIDEOS, est_cache, lister_fichiers, taille_lisible
|
||||
@@ -49,6 +49,7 @@ MOIS = ["janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août
|
||||
JOURS = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"]
|
||||
COULEURS = ["#e6194b", "#3cb44b", "#4363d8", "#f58231", "#911eb4", "#46a0b4", "#f032e6",
|
||||
"#7a9a01", "#c47f00", "#008080", "#9a6324", "#800000", "#2f4b7c", "#808000"]
|
||||
PRESETS = ["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower"]
|
||||
CLES_SOURCES = {"traces": ("Traces GPS", (".gpx",), "traces GPS"),
|
||||
"videos": ("Vidéos", EXT_VIDEOS, "vidéos"),
|
||||
"photos": ("Photos", EXT_PHOTOS, "photos")}
|
||||
@@ -74,6 +75,7 @@ ENV.filters["date_longue"] = _date_longue
|
||||
ENV.filters["date_courte"] = lambda d: f"{d.day} {MOIS[d.month - 1][:4].rstrip('.')}" if d else ""
|
||||
ENV.filters["taille"] = taille_lisible
|
||||
ENV.filters["url"] = lambda s: quote(str(s), safe="")
|
||||
ENV.filters["crf"] = lambda x: f"{x:g}"
|
||||
ENV.tests["commence_par"] = lambda s, debut: str(s).startswith(debut)
|
||||
ENV.globals.update(version=__version__, periode=_periode, qualites=QUALITES,
|
||||
noms_qualites={"econome": "économe", "equilibree": "équilibrée", "haute": "haute"})
|
||||
@@ -124,12 +126,15 @@ class Application:
|
||||
("POST", r"/voyage/dates-suite", self.voyage_dates_suite),
|
||||
("GET", r"/voyage/infos-utiles", self.voyage_infos_utiles),
|
||||
("GET", r"/voyage/carte.json", self.voyage_carte),
|
||||
("GET", r"/voyage/compression", self.voyage_compression),
|
||||
("POST", r"/voyage/video", self.voyage_video),
|
||||
("POST", r"/voyage/position", self.voyage_position),
|
||||
("POST", r"/voyage/masque", self.voyage_masque),
|
||||
("GET", r"/dossiers", self.dossiers),
|
||||
("GET", r"/reglages", self.reglages),
|
||||
("POST", r"/reglages", self.reglages_enregistrer),
|
||||
("GET", r"/systeme", self.systeme),
|
||||
("GET", r"/aide(?:/[a-z]+)?", self.aide),
|
||||
("POST", r"/tache/lancer", self.tache_lancer),
|
||||
("POST", r"/tache/arreter", self.tache_arreter),
|
||||
("GET", r"/tache", self.tache),
|
||||
@@ -361,6 +366,41 @@ class Application:
|
||||
infos["a_encoder_octets"] = sum(os.path.getsize(f) for f in reste if os.path.exists(f))
|
||||
return page("_infos_utiles.html", i=infos, cfg=cfg)
|
||||
|
||||
# --- Compression des vidéos
|
||||
def voyage_compression(self, r, message=""):
|
||||
rel = r.q("v") or r.f("v")
|
||||
chemin = self.chemin_voyage(rel)
|
||||
cfg = self.cfg_voyage(chemin)
|
||||
site_cfg = reglages_site(self.racine)
|
||||
propre = lire_toml(os.path.join(chemin, VOYAGE_TOML)).get("video", {})
|
||||
p = parametres_video(cfg)
|
||||
return page("_compression.html", v={"chemin": rel}, bilan=video.bilan_compression(chemin),
|
||||
p=p, cibles={"1080p": video.cible("hd", p), "720p": video.cible("sd", p)},
|
||||
cfg=cfg, site=site_cfg, site_p=parametres_video(site_cfg), propre=propre,
|
||||
presets=PRESETS, message=message)
|
||||
|
||||
def voyage_video(self, r):
|
||||
chemin = self.chemin_voyage(r.f("v"))
|
||||
changements = {}
|
||||
q = r.f("qualite")
|
||||
changements["qualite"] = q if q in QUALITES else None
|
||||
pr = r.f("preset")
|
||||
changements["preset"] = pr if pr in PRESETS else None
|
||||
for k in ("crf_hevc", "crf_h264"):
|
||||
val = r.f(k).strip()
|
||||
if not val:
|
||||
changements[k] = None
|
||||
continue
|
||||
try:
|
||||
n = int(val)
|
||||
except ValueError:
|
||||
raise Erreur(400, f"CRF invalide : {val}")
|
||||
if not 0 <= n <= 51:
|
||||
raise Erreur(400, "Le CRF va de 0 (sans perte) à 51")
|
||||
changements[k] = n
|
||||
self.toml_voyage(chemin, {"video": changements})
|
||||
return self.voyage_compression(r, message="✓ Réglages vidéo du voyage enregistrés.")
|
||||
|
||||
# --- Carte et corrections
|
||||
def _media_json(self, chemin, rel, m):
|
||||
nom = m["fichier"]
|
||||
@@ -447,6 +487,18 @@ class Application:
|
||||
self.toml_voyage(chemin, {"corrections": {"masques": liste}})
|
||||
return json_rep(self._masques_json(self.cfg_voyage(chemin)))
|
||||
|
||||
# --- Aide (pages Markdown de aide/, aussi lisibles sur git)
|
||||
def aide(self, r):
|
||||
sujet = r.chemin.rstrip("/").rsplit("/", 1)[-1]
|
||||
sujet = "index" if sujet == "aide" else sujet
|
||||
panneau = r.q("panneau") == "1"
|
||||
sujets = _sujets_aide()
|
||||
if sujet not in sujets:
|
||||
raise Erreur(404, "Page d'aide introuvable")
|
||||
html = _aide_html(sujet, self.prefixe, panneau)
|
||||
modele = "_aide.html" if panneau else "aide.html"
|
||||
return page(modele, **self.contexte(sujet=sujet, sujets=sujets, contenu=html))
|
||||
|
||||
# --- Navigateur de dossiers
|
||||
def dossiers(self, r):
|
||||
chemin = r.q("chemin") or os.path.expanduser("~")
|
||||
@@ -527,23 +579,28 @@ class Application:
|
||||
quoi, rel = r.f("quoi"), r.f("v")
|
||||
compression = r.f("sans_compression") != "on"
|
||||
force = r.f("force") == "on"
|
||||
paliers = {"1080p": ("hd",), "720p": ("sd",)}.get(r.f("versions"), ("hd", "sd"))
|
||||
refaire = r.f("refaire") if r.f("refaire") in video.REFAIRE else "manquantes"
|
||||
nom_versions = {"1080p": " (1080p seulement)", "720p": " (720p seulement)"}.get(r.f("versions"), "")
|
||||
racine = self.racine
|
||||
if quoi in ("generer", "compresser"):
|
||||
chemin = self.chemin_voyage(rel)
|
||||
if quoi == "generer":
|
||||
libelle = f"Génération de {rel}" + ("" if compression else " (sans les vidéos)")
|
||||
libelle = f"Génération de {rel}" + (nom_versions if compression else " (sans les vidéos)")
|
||||
|
||||
def fn(suivi):
|
||||
return site.generer(racine, [chemin], compression=compression, force=force, suivi=suivi)
|
||||
return site.generer(racine, [chemin], compression=compression, force=force, suivi=suivi,
|
||||
paliers=paliers, refaire=refaire, force_videos=False)
|
||||
elif quoi == "compresser":
|
||||
libelle = f"Compression des vidéos de {rel}"
|
||||
libelle = f"Compression des vidéos de {rel}{nom_versions}"
|
||||
|
||||
def fn(suivi):
|
||||
cfg = reglages_voyage(chemin, reglages_site(racine))
|
||||
suivi({"etape": "Vidéos", "voyage": rel})
|
||||
return video.compresser(chemin, cfg, sources.analyser(chemin, cfg), force=force,
|
||||
return video.compresser(chemin, cfg, sources.analyser(chemin, cfg), force=False,
|
||||
progression=lambda n, e, p, rg: suivi(
|
||||
{"video": n, "etat": e, "pct": p, "rang": rg}))
|
||||
{"video": n, "etat": e, "pct": p, "rang": rg}),
|
||||
paliers=paliers, refaire=refaire)
|
||||
elif quoi == "tout":
|
||||
libelle = "Génération de tout le site" + ("" if compression else " (sans les vidéos)")
|
||||
|
||||
@@ -603,6 +660,36 @@ def _couches(a, jours, maxi=1500):
|
||||
return couches
|
||||
|
||||
|
||||
AIDE = os.path.join(ICI, "aide")
|
||||
|
||||
|
||||
def _sujets_aide():
|
||||
"""{sujet: titre} des pages d'aide, dans l'ordre du sommaire (index.md)."""
|
||||
titres = {}
|
||||
for nom in os.listdir(AIDE):
|
||||
if nom.endswith(".md"):
|
||||
with open(os.path.join(AIDE, nom), encoding="utf-8") as f:
|
||||
titres[nom[:-3]] = f.readline().lstrip("# ").strip()
|
||||
with open(os.path.join(AIDE, "index.md"), encoding="utf-8") as f:
|
||||
ordre = ["index"] + [s for s in re.findall(r"\]\(([a-z]+)\.md\)", f.read())]
|
||||
ordre += sorted(set(titres) - set(ordre))
|
||||
return {s: titres[s] for s in dict.fromkeys(ordre) if s in titres}
|
||||
|
||||
|
||||
def _aide_html(sujet, base, panneau):
|
||||
import markdown2
|
||||
with open(os.path.join(AIDE, sujet + ".md"), encoding="utf-8") as f:
|
||||
html = markdown2.markdown(f.read(), extras=["tables", "fenced-code-blocks", "cuddled-lists"])
|
||||
|
||||
def lien(m):
|
||||
cible = m.group(1)
|
||||
if panneau:
|
||||
return (f'href="{base}/aide/{cible}" hx-get="{base}/aide/{cible}?panneau=1" '
|
||||
f'hx-target="#aide-contenu"')
|
||||
return f'href="{base}/aide/{cible}"'
|
||||
return re.sub(r'href="([a-z]+)\.md"', lien, html)
|
||||
|
||||
|
||||
def _valeur(d, k, defaut):
|
||||
v = d.get(k, defaut)
|
||||
if isinstance(v, list):
|
||||
@@ -611,8 +698,8 @@ def _valeur(d, k, defaut):
|
||||
|
||||
|
||||
class Requete:
|
||||
def __init__(self, query, form):
|
||||
self.query, self.form = query, form
|
||||
def __init__(self, query, form, chemin=""):
|
||||
self.query, self.form, self.chemin = query, form, chemin
|
||||
|
||||
def q(self, k, defaut=""):
|
||||
return _valeur(self.query, k, defaut)
|
||||
@@ -727,7 +814,7 @@ def creer_gestionnaire(app, port, gardien=None):
|
||||
"Set-Cookie": gardien.entete_cookie("", self.https(), effacer=True)}), tete)
|
||||
if methode == "GET" and chemin.startswith("/site/"):
|
||||
return self.fichier(app.racine, chemin[len("/site/"):], tete)
|
||||
req = Requete(query, form)
|
||||
req = Requete(query, form, chemin)
|
||||
for m, motif, fn in app.routes:
|
||||
if m == methode and motif.match(chemin):
|
||||
rep = fn(req)
|
||||
@@ -940,7 +1027,12 @@ def lancer(racine, port=8765, ouvrir=True, distant=False, certificat=None, cle=N
|
||||
try:
|
||||
serveur.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
taches.arreter()
|
||||
# Un encodage en cours : on arrête proprement ffmpeg (sinon il continuerait
|
||||
# seul et laisserait des fichiers .tmp-).
|
||||
if taches.arreter():
|
||||
fin = time.time() + 15
|
||||
while taches.COURANTE.en_cours and time.time() < fin:
|
||||
time.sleep(0.2)
|
||||
print("\nInterface arrêtée.")
|
||||
finally:
|
||||
serveur.server_close()
|
||||
Reference in new issue
Block a user