- 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
247 lines
11 KiB
Python
247 lines
11 KiB
Python
"""Orchestration : trouver les voyages, les générer, dire où ils en sont."""
|
|
import os
|
|
|
|
from rtg import accueil, extraction, pages, photos, sources, video
|
|
from rtg.config import (SITE_TOML, VOYAGE_TOML, ecrire_toml, nom_lisible,
|
|
reglages_site, reglages_voyage)
|
|
from rtg.outils import dire, est_cache, verifier_arret
|
|
|
|
IGNORES = {"Scripts", "RoadTripsGenerator", "html", "assets", "videos", "photos", "photos_web",
|
|
"apercus", "rushs", "routes"}
|
|
MARQUEURS = ("rushs", "routes", "photos", VOYAGE_TOML)
|
|
|
|
|
|
def _ignore(nom):
|
|
return nom in IGNORES or nom.startswith(("_", "."))
|
|
|
|
|
|
def est_voyage(chemin):
|
|
return any(os.path.lexists(os.path.join(chemin, m)) for m in MARQUEURS)
|
|
|
|
|
|
def decouvrir(racine):
|
|
"""Voyages du site : dossiers de la racine, ou un niveau plus bas dans un
|
|
dossier de regroupement (Balades/…)."""
|
|
res = []
|
|
for d in sorted(os.listdir(racine)):
|
|
chemin = os.path.join(racine, d)
|
|
if _ignore(d) or not os.path.isdir(chemin):
|
|
continue
|
|
if est_voyage(chemin):
|
|
res.append(chemin)
|
|
continue
|
|
for s in sorted(os.listdir(chemin)):
|
|
sous = os.path.join(chemin, s)
|
|
if not _ignore(s) and os.path.isdir(sous) and est_voyage(sous):
|
|
res.append(sous)
|
|
return res
|
|
|
|
|
|
def _sources_disponibles(voyage, liste):
|
|
"""Faux si une source est un lien cassé ou un chemin absent (disque débranché)."""
|
|
for s in liste:
|
|
p = os.path.join(voyage, os.path.expanduser(s))
|
|
if not os.path.exists(p) and (os.path.islink(p) or s not in ("rushs", "routes", "photos")):
|
|
return False
|
|
return True
|
|
|
|
|
|
def generer_voyage(voyage, racine, site_cfg, compression=True, force=False, suivi=None,
|
|
paliers=("hd", "sd"), refaire="manquantes", force_videos=None):
|
|
"""Toute la chaîne pour un voyage. `force` refait photos web et pages, et
|
|
aussi les vidéos sauf si force_videos=False (l'interface choisit alors les
|
|
vidéos à refaire par `refaire`). `suivi(evenement)` (interface) reçoit
|
|
{"etape": …} au début de chaque étape et {"video": …} pendant la compression."""
|
|
cfg = reglages_voyage(voyage, site_cfg)
|
|
nom = os.path.relpath(voyage, racine)
|
|
dire("=" * 50)
|
|
dire(f"▶ {nom}")
|
|
dire("=" * 50)
|
|
|
|
def etape(libelle):
|
|
verifier_arret()
|
|
if suivi:
|
|
suivi({"etape": libelle, "voyage": nom})
|
|
|
|
etape("Sources")
|
|
a = sources.analyser(voyage, cfg)
|
|
for m in a.a_verifier:
|
|
dire(f"⚠️ {m}")
|
|
for r in sources.ranger(voyage, cfg, a):
|
|
dire(r)
|
|
|
|
etape("Positions")
|
|
extraction.extraire_photos(voyage, cfg, a)
|
|
if _sources_disponibles(voyage, cfg["sources"]["videos"]):
|
|
extraction.extraire_videos(voyage, a)
|
|
else:
|
|
dire("⏭️ Vidéos sources inaccessibles : positions reprises du dernier export.")
|
|
|
|
if compression and a.videos:
|
|
etape("Vidéos")
|
|
progression = None
|
|
if suivi:
|
|
def progression(fichier, etat, pct, rang):
|
|
suivi({"video": fichier, "etat": etat, "pct": pct, "rang": rang})
|
|
video.compresser(voyage, cfg, a, force=force if force_videos is None else force_videos,
|
|
progression=progression, paliers=paliers, refaire=refaire)
|
|
elif not compression:
|
|
dire("⏭️ Compression sautée.")
|
|
|
|
etape("Photos")
|
|
photos.preparer(voyage, cfg, force=force)
|
|
etape("Pages")
|
|
ok = pages.generer(voyage, racine, cfg["adresse_publique"], cfg)
|
|
dire()
|
|
return ok
|
|
|
|
|
|
def generer(racine, voyages=None, compression=True, force=False, mymaps_maj=False, suivi=None,
|
|
paliers=("hd", "sd"), refaire="manquantes", force_videos=None):
|
|
racine = os.path.abspath(racine)
|
|
cfg = reglages_site(racine)
|
|
liste = [os.path.abspath(v) for v in voyages] if voyages else decouvrir(racine)
|
|
if not liste:
|
|
dire("❌ Aucun voyage trouvé (dossiers avec routes/, photos/, rushs/ ou voyage.toml).")
|
|
return False
|
|
dire(f"📁 {len(liste)} voyage(s) : {', '.join(os.path.relpath(v, racine) for v in liste)}\n")
|
|
echecs = [v for v in liste
|
|
if not generer_voyage(v, racine, cfg, compression, force, suivi, paliers, refaire,
|
|
force_videos)]
|
|
verifier_arret()
|
|
if suivi:
|
|
suivi({"etape": "Accueil", "voyage": ""})
|
|
dire("=" * 50)
|
|
dire("▶ Accueil")
|
|
dire("=" * 50)
|
|
accueil.generer(racine, cfg, mymaps_maj)
|
|
dire(f"\n✅ {len(liste) - len(echecs)} voyage(s) généré(s)"
|
|
+ (f", ❌ {len(echecs)} en échec" if echecs else ""))
|
|
return not echecs
|
|
|
|
|
|
def rubrique_de(voyage, racine):
|
|
parent = os.path.dirname(os.path.abspath(voyage))
|
|
return "Voyages" if parent == os.path.abspath(racine) else nom_lisible(os.path.basename(parent))
|
|
|
|
|
|
def etat_voyage(v, racine, site_cfg):
|
|
"""Avancement d'un voyage (dictionnaire), sans rien lire de lourd."""
|
|
cfg = reglages_voyage(v, site_cfg)
|
|
a = sources.analyser(v, cfg, leger=True)
|
|
dossier = os.path.join(v, "videos")
|
|
faites_web = len([f for f in os.listdir(dossier) if f.endswith("_mobile.mp4")]) if os.path.isdir(dossier) else 0
|
|
if a.videos:
|
|
faites = sum(1 for s in a.videos if video.deja_faite(v, s, rapide=True))
|
|
vid = f"{faites}/{len(a.videos)}"
|
|
else:
|
|
faites = None
|
|
vid = f"{faites_web} (sources absentes)" if faites_web else "—"
|
|
ph = os.path.join(v, "photos")
|
|
nb_ph = len([f for f in os.listdir(ph) if f.lower().endswith((".jpg", ".jpeg"))]) if os.path.isdir(ph) else 0
|
|
pw = os.path.join(v, "photos_web")
|
|
nb_pw = len([f for f in os.listdir(pw) if not est_cache(f)]) if os.path.isdir(pw) else 0
|
|
index = os.path.join(v, "index.html")
|
|
genere = os.path.exists(index)
|
|
pages_ok = genere and all(
|
|
os.path.getmtime(index) >= os.path.getmtime(os.path.join(v, c))
|
|
for c in ("export_photos.csv", "export_videos.csv", "voyage.md")
|
|
if os.path.exists(os.path.join(v, c)))
|
|
jours = sorted(a.journees)
|
|
return {
|
|
"chemin": os.path.relpath(v, racine).replace(os.sep, "/"), "dossier": v,
|
|
"titre": cfg["titre"], "moto": cfg.get("moto", ""), "rubrique": rubrique_de(v, racine),
|
|
"debut": jours[0] if jours else None, "fin": jours[-1] if jours else None,
|
|
"jours": len(jours), "videos": vid, "videos_sources": len(a.videos),
|
|
"videos_faites": faites, "videos_web": faites_web,
|
|
"photos": nb_ph, "photos_web": nb_pw, "genere": genere, "pages_a_jour": pages_ok,
|
|
"a_verifier": a.a_verifier,
|
|
}
|
|
|
|
|
|
def tableau_etat(racine):
|
|
racine = os.path.abspath(racine)
|
|
site_cfg = reglages_site(racine)
|
|
return [etat_voyage(v, racine, site_cfg) for v in decouvrir(racine)]
|
|
|
|
|
|
def etat(racine):
|
|
"""Tableau de l'avancement de chaque voyage."""
|
|
lignes = [("Voyage", "Jours", "Vidéos web", "Photos web", "Pages", "À vérifier")]
|
|
for e in tableau_etat(racine):
|
|
lignes.append((e["chemin"], str(e["jours"]), e["videos"],
|
|
f"{e['photos_web']}/{e['photos']}" if e["photos"] else "—",
|
|
"à jour" if e["pages_a_jour"] else "à refaire", str(len(e["a_verifier"]))))
|
|
largeurs = [max(len(l[i]) for l in lignes) for i in range(len(lignes[0]))]
|
|
for k, l in enumerate(lignes):
|
|
dire(" ".join(c.ljust(w) for c, w in zip(l, largeurs)))
|
|
if k == 0:
|
|
dire(" ".join("-" * w for w in largeurs))
|
|
|
|
|
|
def analyser(voyage, racine):
|
|
cfg = reglages_voyage(os.path.abspath(voyage), reglages_site(racine))
|
|
a = sources.analyser(os.path.abspath(voyage), cfg)
|
|
dire(f"Voyage : {cfg['titre']}" + (f" ({cfg['moto']})" if cfg.get("moto") else ""))
|
|
dire(f"Traces : {len(a.traces)} fichier(s), {len(a.journees)} journée(s)")
|
|
for jour in sorted(a.journees):
|
|
dire(f" {jour.isoformat()} {a.km(jour):6.0f} km")
|
|
taille = sum(os.path.getsize(v) for v in a.videos) / 1e9
|
|
dire(f"Photos : {len(a.photos)}")
|
|
dire(f"Vidéos : {len(a.videos)} ({taille:.1f} Go)")
|
|
if a.a_verifier:
|
|
dire("À vérifier :")
|
|
for m in a.a_verifier:
|
|
dire(f" ⚠️ {m}")
|
|
|
|
|
|
def creer_voyage(racine, dossier, titre, groupe="", moto=""):
|
|
"""Nouveau voyage : <racine>/[<groupe>/]<dossier>/voyage.toml, sans source.
|
|
Renvoie le chemin du dossier créé."""
|
|
for nom in (dossier, groupe):
|
|
if nom and (os.sep in nom or "/" in nom or nom.startswith((".", "_")) or nom in IGNORES):
|
|
raise ValueError(f"Nom de dossier refusé : {nom}")
|
|
chemin = os.path.join(racine, groupe, dossier) if groupe else os.path.join(racine, dossier)
|
|
if os.path.exists(chemin):
|
|
raise ValueError(f"Le dossier {os.path.relpath(chemin, racine)} existe déjà")
|
|
os.makedirs(chemin)
|
|
donnees = {"titre": titre or nom_lisible(dossier)}
|
|
if moto:
|
|
donnees["moto"] = moto
|
|
donnees["sources"] = {"traces": [], "videos": [], "photos": []}
|
|
ecrire_toml(os.path.join(chemin, VOYAGE_TOML), donnees,
|
|
entete="Voyage RoadTripsGenerator : sources, infos et réglages propres.")
|
|
return chemin
|
|
|
|
|
|
def migrer(racine):
|
|
"""Écrit site.toml et un voyage.toml par voyage à partir de l'existant
|
|
(mymaps.txt, moto.txt). Ne remplace jamais un fichier déjà présent."""
|
|
racine = os.path.abspath(racine)
|
|
cfg = reglages_site(racine)
|
|
ecrits = []
|
|
if not os.path.exists(os.path.join(racine, SITE_TOML)):
|
|
ecrire_toml(os.path.join(racine, SITE_TOML), {
|
|
"titre": cfg["titre"], "adresse_publique": cfg["adresse_publique"],
|
|
"accueil_nb_recents": cfg["accueil_nb_recents"],
|
|
"video": {"qualite": cfg["video"]["qualite"], "encodeur": cfg["video"]["encodeur"]},
|
|
"photos": {"grand_cote": cfg["photos"]["grand_cote"], "qualite": cfg["photos"]["qualite"]},
|
|
"journees": dict(cfg["journees"]),
|
|
"carte_globale": {"mymaps": cfg["carte_globale"]["mymaps"]},
|
|
}, entete="Réglages du site RoadTripsGenerator (rtg).")
|
|
ecrits.append(SITE_TOML)
|
|
for v in decouvrir(racine):
|
|
chemin = os.path.join(v, VOYAGE_TOML)
|
|
if os.path.exists(chemin):
|
|
continue
|
|
vc = reglages_voyage(v, cfg)
|
|
parent = os.path.dirname(v)
|
|
donnees = {"titre": nom_lisible(os.path.basename(v)),
|
|
"rubrique": "Voyages" if parent == racine else nom_lisible(os.path.basename(parent))}
|
|
if vc.get("moto"):
|
|
donnees["moto"] = vc["moto"]
|
|
donnees["sources"] = {"traces": ["routes"], "videos": ["rushs"], "photos": ["photos"]}
|
|
ecrire_toml(chemin, donnees, entete="Voyage RoadTripsGenerator : sources, infos et réglages propres.")
|
|
ecrits.append(os.path.relpath(chemin, racine))
|
|
dire(f"✅ {len(ecrits)} fichier(s) écrit(s)" + ("".join(f"\n {e}" for e in ecrits)))
|