Files
RoadTripsGenerator/rtg/site.py
T
FabriceandClaude Opus 5.5 f0e1c2dfce Phase 1 : moteur Python et commande rtg
- Paquet roadtripsgenerator (pyproject.toml), commande rtg :
  generer, etat, analyser, compresser, accueil, migrer
- Réglages site.toml / voyage.toml (anciens mymaps.txt et moto.txt
  encore lus) ; qualité vidéo en trois crans
- Sources laissées en place : traces découpées par journée d'après
  leurs horodatages (coupure à 4 h) ou la date du nom de fichier,
  photos copiées, HEIC converties en JPEG, position déduite de la
  trace pour les photos sans GPS
- extraction_infos.sh et compress.sh portés en Python (plus de bash) ;
  sorties vidéo écrites sous un nom temporaire ; positions des rushs
  inchangés reprises du dernier export ; export vidéo conservé quand
  le disque des rushs est débranché
- Pages, accueil et carte globale repris dans le paquet : sortie
  identique aux scripts (123 fichiers comparés sur 7 voyages)
- Tests pytest sur un voyage d'exemple fabriqué à la volée

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

176 lines
7.5 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
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):
cfg = reglages_voyage(voyage, site_cfg)
nom = os.path.relpath(voyage, racine)
dire("=" * 50)
dire(f"▶ {nom}")
dire("=" * 50)
a = sources.analyser(voyage, cfg)
for m in a.a_verifier:
dire(f"⚠️ {m}")
for r in sources.ranger(voyage, cfg, a):
dire(r)
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:
video.compresser(voyage, cfg, a, force=force)
elif not compression:
dire("⏭️ Compression sautée.")
photos.preparer(voyage, cfg, force=force)
ok = pages.generer(voyage, racine, cfg["adresse_publique"])
dire()
return ok
def generer(racine, voyages=None, compression=True, force=False, mymaps_maj=False):
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)]
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 etat(racine):
"""Tableau de l'avancement de chaque voyage."""
racine = os.path.abspath(racine)
site_cfg = reglages_site(racine)
lignes = [("Voyage", "Jours", "Vidéos web", "Photos web", "Pages", "À vérifier")]
for v in decouvrir(racine):
cfg = reglages_voyage(v, site_cfg)
a = sources.analyser(v, cfg)
if a.videos:
faites = sum(1 for s in a.videos if video.deja_faite(v, s))
vid = f"{faites}/{len(a.videos)}"
else:
dossier = os.path.join(v, "videos")
n = len([f for f in os.listdir(dossier) if f.endswith("_mobile.mp4")]) if os.path.isdir(dossier) else 0
vid = f"{n} (sources absentes)" if n 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")
pages_ok = "à jour" if os.path.exists(index) 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))) else "à refaire"
lignes.append((os.path.relpath(v, racine), str(len(a.journees)), vid,
f"{nb_pw}/{nb_ph}" if nb_ph else "—", pages_ok, str(len(a.a_verifier)) or "0"))
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 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)))