Files
FabriceandClaude Opus 5.5 97b936fd3e Refonte : templates, balades, photos web, aperçus, carte globale
- Mise en page sortie du Python : Scripts/templates/ (Jinja2 + CSS/JS
  communs copiés dans assets/ de chaque voyage)
- Voyages rangés dans un sous-dossier (Balades/…) : rubriques sur
  l'accueil, 2 plus récents par rubrique, pages voyages.html et
  balades.html
- prepare_web.py : photos allégées (photos_web/) et images d'aperçu
  des vidéos (apercus/)
- Voyage d'une seule journée : la page du voyage est la carte du jour
- Carte globale de l'accueil (OpenStreetMap) : voyages du site + cartes
  Google My Maps récupérées (mymaps.txt, _mymaps/), moto par voyage
  (moto.txt)
- KML par voyage pour My Maps
- compress.sh : réencode une sortie illisible (encodage interrompu) ;
  repli automatique sur l'autre qualité dans la page
- Corrections : CSV vide (voyage sans photo), racine de genere_site.sh,
  fichiers ._ de macOS
- README réécrit, commentaires allégés

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

193 lines
7.3 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Prépare les médias d'un voyage pour le web, sans toucher aux originaux.
photos_web/<nom> photos réduites (WEB_PX px de grand côté), affichées
dans les diaporamas ; le clic ouvre toujours l'original
apercus/<base>.jpg image d'aperçu de chaque vidéo, affichée avant lecture
Usage : ./prepare_web.py [dossier_voyage]
Variables :
WEB_PX=1600 grand côté des photos web (un popup fait au plus 800 px
de large, ×2 pour les écrans Retina)
WEB_QUALITE=80 qualité JPEG des photos web
FORCE=1 refait tout, même ce qui est à jour
Un fichier déjà produit et plus récent que sa source n'est pas refait.
Photos : Pillow (pip install pillow), sinon `sips` (fourni avec macOS).
Aperçus : ffmpeg.
"""
import os
import shutil
import subprocess
import sys
from concurrent.futures import ThreadPoolExecutor
try:
from PIL import Image, ImageOps
except ImportError:
Image = None
WEB_PX = int(os.environ.get("WEB_PX", 1600))
QUALITE = int(os.environ.get("WEB_QUALITE", 80))
FORCE = os.environ.get("FORCE") == "1"
TRAVAILLEURS = os.cpu_count() or 4
def a_jour(sortie, source):
return (not FORCE and os.path.exists(sortie) and os.path.getsize(sortie) > 0
and os.path.getmtime(sortie) >= os.path.getmtime(source))
def lister(dossier, extensions):
"""Fichiers du dossier ayant l'une des extensions, hors fichiers cachés et « ._ » de macOS."""
if not os.path.isdir(dossier):
return []
return sorted(f for f in os.listdir(dossier)
if not f.startswith('.') and f.lower().endswith(extensions))
def nettoyer_temporaires(dossier):
"""Restes d'une exécution interrompue (Ctrl-C) : fichiers .tmp-* cachés."""
for f in os.listdir(dossier):
if f.startswith(".tmp-"):
os.remove(os.path.join(dossier, f))
# --- Photos ------------------------------------------------------------------
def reduire_pillow(src, dst):
with Image.open(src) as im:
im = ImageOps.exif_transpose(im) # applique la rotation EXIF
if im.mode not in ("RGB", "L"):
im = im.convert("RGB")
im.thumbnail((WEB_PX, WEB_PX), Image.LANCZOS)
im.save(dst, "JPEG", quality=QUALITE, optimize=True, progressive=True)
def reduire_sips(src, dst):
subprocess.run(["sips", "-Z", str(WEB_PX), "-s", "format", "jpeg",
"-s", "formatOptions", str(QUALITE), src, "--out", dst],
check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def preparer_photos(voyage):
src_dir = os.path.join(voyage, "photos")
dst_dir = os.path.join(voyage, "photos_web")
photos = lister(src_dir, (".jpg", ".jpeg"))
if not photos:
print("📸 Photos web : aucune photo.")
return True
if Image is not None:
reduire = reduire_pillow
elif shutil.which("sips"):
reduire = reduire_sips
else:
print("⚠️ Photos web : ni Pillow (pip install pillow) ni sips disponible,"
" les pages afficheront les originaux.")
return False
os.makedirs(dst_dir, exist_ok=True)
nettoyer_temporaires(dst_dir)
a_faire = [n for n in photos if not a_jour(os.path.join(dst_dir, n), os.path.join(src_dir, n))]
def traiter(nom):
src, dst = os.path.join(src_dir, nom), os.path.join(dst_dir, nom)
# Écrit sous un nom caché puis renommé : jamais de fichier à moitié écrit
# sous le vrai nom. L'extension est gardée, sips s'en sert.
tmp = os.path.join(dst_dir, ".tmp-" + nom)
try:
reduire(src, tmp)
os.replace(tmp, dst)
return os.path.getsize(src), os.path.getsize(dst)
except Exception as e:
print(f" ❌ {nom} : {e}")
if os.path.exists(tmp):
os.remove(tmp)
return None
with ThreadPoolExecutor(TRAVAILLEURS) as pool:
resultats = [r for r in pool.map(traiter, a_faire) if r]
msg = f"📸 Photos web : {len(resultats)} réduite(s), {len(photos) - len(a_faire)} déjà à jour"
if resultats:
avant = sum(r[0] for r in resultats) / 1048576
apres = sum(r[1] for r in resultats) / 1048576
msg += f" ({avant:.0f} Mo → {apres:.0f} Mo)"
print(msg)
return len(resultats) == len(a_faire)
# --- Aperçus vidéo -----------------------------------------------------------
def extraire_apercu(sources, dst):
"""Une image à 3 s du début (après les secousses du démarrage), à 0 s si la
vidéo est plus courte. Sources essayées dans l'ordre : le 720p se décode
bien plus vite, le 1080p sert de secours s'il est abîmé.
Renvoie la source utilisée, ou None."""
tmp = os.path.join(os.path.dirname(dst), ".tmp-" + os.path.basename(dst))
for src in sources:
for instant in ("3", "0"):
subprocess.run(["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error", "-y",
"-ss", instant, "-i", src, "-frames:v", "1",
"-vf", "scale=-2:540", "-q:v", "5", "-f", "mjpeg", tmp],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if os.path.exists(tmp) and os.path.getsize(tmp) > 0:
os.replace(tmp, dst)
return src
if os.path.exists(tmp):
os.remove(tmp)
return None
def preparer_apercus(voyage):
src_dir = os.path.join(voyage, "videos")
dst_dir = os.path.join(voyage, "apercus")
fichiers = lister(src_dir, (".mp4", ".mov"))
if not fichiers:
print("🎞️ Aperçus : aucune vidéo.")
return True
if not shutil.which("ffmpeg"):
print("⚠️ Aperçus : ffmpeg introuvable, vidéos sans image d'aperçu.")
return False
# Une vidéo = un nom de base, avec jusqu'à deux fichiers ; le 720p d'abord.
sources = {}
for f in fichiers:
base = os.path.splitext(f)[0]
mobile = base.endswith("_mobile")
if mobile:
base = base[:-len("_mobile")]
liste = sources.setdefault(base, [])
liste.insert(0 if mobile else len(liste), os.path.join(src_dir, f))
os.makedirs(dst_dir, exist_ok=True)
nettoyer_temporaires(dst_dir)
a_faire = [(srcs, os.path.join(dst_dir, b + ".jpg")) for b, srcs in sorted(sources.items())
if not a_jour(os.path.join(dst_dir, b + ".jpg"), srcs[0])]
with ThreadPoolExecutor(TRAVAILLEURS) as pool:
utilisees = list(pool.map(lambda t: extraire_apercu(*t), a_faire))
for (srcs, _), src in zip(a_faire, utilisees):
if src is None:
print(f" ❌ aperçu impossible, vidéo illisible : {os.path.basename(srcs[0])}")
elif src != srcs[0]:
print(f" ⚠️ {os.path.basename(srcs[0])} illisible (encodage interrompu ?),"
f" aperçu tiré de {os.path.basename(src)}. Relance compress.sh pour le refaire.")
ok = [u for u in utilisees if u]
print(f"🎞️ Aperçus : {len(ok)} créé(s), {len(sources) - len(a_faire)} déjà à jour")
return len(ok) == len(a_faire)
def main():
voyage = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else ".")
print(f"--- PRÉPARATION WEB : {os.path.basename(voyage)} ---")
ok = preparer_photos(voyage)
ok = preparer_apercus(voyage) and ok
sys.exit(0 if ok else 1)
if __name__ == "__main__":
main()