- 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
124 lines
5.6 KiB
Python
124 lines
5.6 KiB
Python
"""Chaîne complète sur un petit voyage fabriqué à la volée.
|
|
|
|
Sources « mal rangées » exprès : une trace de deux jours au nom quelconque,
|
|
des photos dans un autre dossier (dont une HEIC et une sans GPS), une vidéo.
|
|
"""
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
import pytest
|
|
|
|
from rtg import config, site, sources
|
|
|
|
pytestmark = pytest.mark.skipif(not (shutil.which("exiftool") and shutil.which("ffmpeg")),
|
|
reason="exiftool et ffmpeg nécessaires")
|
|
|
|
UTC = timezone.utc
|
|
|
|
|
|
def _gpx(chemin):
|
|
"""Deux journées (Paris, UTC+2 en mai) ; un point à 00 h 30 locale doit
|
|
rester sur la veille (coupure à 4 h)."""
|
|
pts = []
|
|
t0 = datetime(2026, 5, 1, 6, 0, tzinfo=UTC) # 8 h locale
|
|
for i in range(60):
|
|
pts.append((48.80 + i * 0.002, 2.30 + i * 0.003, t0 + timedelta(minutes=2 * i)))
|
|
pts.append((48.95, 2.50, datetime(2026, 5, 1, 22, 30, tzinfo=UTC))) # 00 h 30 le 2 mai
|
|
t1 = datetime(2026, 5, 2, 7, 0, tzinfo=UTC)
|
|
for i in range(40):
|
|
pts.append((49.00 + i * 0.002, 2.60 + i * 0.002, t1 + timedelta(minutes=3 * i)))
|
|
seg = "\n".join(f'<trkpt lat="{la}" lon="{lo}"><time>{t.strftime("%Y-%m-%dT%H:%M:%SZ")}</time></trkpt>'
|
|
for la, lo, t in pts)
|
|
with open(chemin, "w") as f:
|
|
f.write(f'<?xml version="1.0"?><gpx version="1.1" creator="test" '
|
|
f'xmlns="http://www.topografix.com/GPX/1/1"><trk><name>test</name><trkseg>{seg}'
|
|
f'</trkseg></trk></gpx>')
|
|
|
|
|
|
def _photo(chemin, date_locale, gps=None):
|
|
from PIL import Image
|
|
im = Image.new("RGB", (3000, 2000), (90, 140, 200))
|
|
im.save(chemin, "JPEG")
|
|
args = ["exiftool", "-q", "-overwrite_original", f"-DateTimeOriginal={date_locale}"]
|
|
if gps:
|
|
args += [f"-GPSLatitude={gps[0]}", "-GPSLatitudeRef=N", f"-GPSLongitude={gps[1]}", "-GPSLongitudeRef=E"]
|
|
subprocess.run(args + [chemin], check=True)
|
|
|
|
|
|
def _heic(chemin, jpeg):
|
|
from PIL import Image
|
|
import pillow_heif
|
|
pillow_heif.register_heif_opener()
|
|
with Image.open(jpeg) as im:
|
|
im.save(chemin, format="HEIF", exif=im.info.get("exif"))
|
|
|
|
|
|
@pytest.fixture
|
|
def site_test(tmp_path):
|
|
racine = tmp_path / "site"
|
|
voyage = racine / "Balades" / "Test-05-2026"
|
|
sources_dir = tmp_path / "carte_sd"
|
|
for d in (voyage, sources_dir / "photos", sources_dir / "gopro"):
|
|
d.mkdir(parents=True)
|
|
_gpx(sources_dir / "export_68degres_quelconque.gpx")
|
|
_photo(str(sources_dir / "photos" / "IMG_0001.jpg"), "2026:05:01 09:30:00", gps=(48.85, 2.35))
|
|
# Sans GPS, prise à 9 h 00 locale le 1er mai = 7 h 00 UTC : 30e point de la trace
|
|
_photo(str(sources_dir / "photos" / "IMG_0002.jpg"), "2026:05:01 09:00:00")
|
|
_photo(str(tmp_path / "base.jpg"), "2026:05:02 10:00:00", gps=(49.01, 2.61))
|
|
_heic(str(sources_dir / "photos" / "IMG_0003.HEIC"), str(tmp_path / "base.jpg"))
|
|
subprocess.run(["ffmpeg", "-loglevel", "error", "-f", "lavfi", "-i", "testsrc=size=1280x720:rate=30",
|
|
"-f", "lavfi", "-i", "sine=frequency=440", "-t", "2", "-c:v", "libx264", "-c:a", "aac",
|
|
"-shortest", str(sources_dir / "gopro" / "GX010001.MP4")], check=True)
|
|
config.ecrire_toml(str(racine / "site.toml"), {"titre": "Site de test",
|
|
"video": {"preset": "ultrafast"}})
|
|
config.ecrire_toml(str(voyage / "voyage.toml"), {
|
|
"titre": "Test", "moto": "Moto d'essai",
|
|
"sources": {"traces": [str(sources_dir / "export_68degres_quelconque.gpx")],
|
|
"videos": [str(sources_dir / "gopro")],
|
|
"photos": [str(sources_dir / "photos")]}})
|
|
return racine, voyage
|
|
|
|
|
|
def test_decoupage_et_coupure(site_test):
|
|
racine, voyage = site_test
|
|
cfg = config.reglages_voyage(str(voyage), config.reglages_site(str(racine)))
|
|
a = sources.analyser(str(voyage), cfg)
|
|
assert sorted(j.isoformat() for j in a.journees) == ["2026-05-01", "2026-05-02"]
|
|
# Le point de 00 h 30 (heure de Paris) appartient au 1er mai
|
|
derniers = [p for t in a.journees[min(a.journees)] for s in t.segments for p in s.points]
|
|
assert derniers[-1].latitude == 48.95
|
|
|
|
|
|
def test_chaine_complete(site_test):
|
|
racine, voyage = site_test
|
|
assert site.generer(str(racine))
|
|
# Traces rangées par jour
|
|
assert sorted(os.listdir(voyage / "routes")) == ["2026-05-01.gpx", "2026-05-02.gpx"]
|
|
# Photos copiées, HEIC convertie en JPEG
|
|
assert sorted(os.listdir(voyage / "photos")) == ["IMG_0001.jpg", "IMG_0002.jpg", "IMG_0003.jpeg"]
|
|
csv = (voyage / "export_photos.csv").read_text()
|
|
assert "IMG_0001.jpg" in csv and "IMG_0003.jpeg" in csv
|
|
ligne2 = [l for l in csv.splitlines() if "IMG_0002" in l][0]
|
|
lat = float(ligne2.split(",")[2])
|
|
assert abs(lat - (48.80 + 30 * 0.002)) < 0.003 # position déduite de la trace
|
|
# Vidéo compressée en deux paliers lisibles
|
|
assert (voyage / "videos" / "GX010001.mp4").exists()
|
|
assert (voyage / "videos" / "GX010001_mobile.mp4").exists()
|
|
# Pages
|
|
assert sorted(os.listdir(voyage / "html")) == ["carte_2026-05-01.html", "carte_2026-05-02.html"]
|
|
assert (voyage / "index.html").exists() and (voyage / "photos_web" / "IMG_0001.jpg").exists()
|
|
accueil = (racine / "index.html").read_text()
|
|
assert "Site de test" in accueil and "Balades/Test-05-2026/index.html" in accueil
|
|
|
|
|
|
def test_deuxieme_passage_ne_refait_rien(site_test, capsys):
|
|
racine, voyage = site_test
|
|
site.generer(str(racine))
|
|
capsys.readouterr()
|
|
site.generer(str(racine))
|
|
sortie = capsys.readouterr().out
|
|
assert "Aucun nouvel encodage" in sortie
|
|
assert "0 réduite(s)" in sortie
|