"""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): """Toute la chaîne pour un voyage. `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, progression=progression) 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): 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)] 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 : /[/]/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)))