Compare commits
2
Commits
b54ae69b19
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f08aa09449 | ||
|
|
ada3d76f50 |
No files matched your search
@@ -1,2 +1,5 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
*.egg-info/
|
||||||
|
rtg-admin.log
|
||||||
|
rtg-admin.pid
|
||||||
+1
-1
@@ -17,7 +17,7 @@ RUN pip install --no-cache-dir . && rtg verifier
|
|||||||
# le Mac : les sources notées dans les voyage.toml restent valables.
|
# le Mac : les sources notées dans les voyage.toml restent valables.
|
||||||
ENV XDG_CONFIG_HOME=/config TZ=Europe/Paris PYTHONUNBUFFERED=1 \
|
ENV XDG_CONFIG_HOME=/config TZ=Europe/Paris PYTHONUNBUFFERED=1 \
|
||||||
RTG_RACINE=/Volumes/CLEROADTRIPS/Road-Trips RTG_PREFIXE=/admin \
|
RTG_RACINE=/Volumes/CLEROADTRIPS/Road-Trips RTG_PREFIXE=/admin \
|
||||||
RTG_ARRET_INACTIF=120
|
RTG_ARRET_INACTIF=0
|
||||||
EXPOSE 8765
|
EXPOSE 8765
|
||||||
|
|
||||||
# Aucun port publié : seul le nginx du site (même réseau Docker) le joint.
|
# Aucun port publié : seul le nginx du site (même réseau Docker) le joint.
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ Je me suis rendu compte que je passais des heures à monter des films de mes roa
|
|||||||
|
|
||||||
Pour chaque voyage, je dispose de photos et de vidéos géolocalisées (photos iPhone, films GoPro), des traces GPS de chaque journée et d'un récit écrit en Markdown.
|
Pour chaque voyage, je dispose de photos et de vidéos géolocalisées (photos iPhone, films GoPro), des traces GPS de chaque journée et d'un récit écrit en Markdown.
|
||||||
|
|
||||||
Les scripts lisent les positions GPS des photos et des vidéos, les placent sur une carte avec la trace du parcours découpée par jour, et construisent des pages web où l'on peut, en parallèle, lire le récit du voyage. Voir par exemple mon [road-trip de l'été 2025](https://roadtrip.ouaibe.fr/Ete-2025/).
|
RTG lit les positions GPS des photos et des vidéos, les placent sur une carte avec la trace du parcours découpée par jour, et construit des pages web où l'on peut, en parallèle, lire le récit du voyage. Voir par exemple mon [road-trip de l'été 2025](https://roadtrips.ouaibe.fr/Ete-2025/).
|
||||||
|
|
||||||
Ils préparent aussi les médias pour le web :
|
Il prépare aussi les médias pour le web :
|
||||||
|
|
||||||
* **vidéos** : un 720p H.264 lisible partout, chargé par défaut, et un 1080p HEVC proposé à la demande (environ 40 % plus léger que du H.264 à qualité égale) ;
|
* **vidéos** : un 720p H.264 lisible partout, chargé par défaut, et un 1080p HEVC proposé à la demande (environ 40 % plus léger que du H.264 à qualité égale) ;
|
||||||
* **photos** : une version allégée (1600 px, quelques centaines de Ko) affichée dans les diaporamas, le clic ouvrant l'original ;
|
* **photos** : une version allégée (1600 px, quelques centaines de Ko) affichée dans les diaporamas, le clic ouvrant l'original ;
|
||||||
@@ -86,212 +86,177 @@ rtg verifier
|
|||||||
|
|
||||||
La commande contrôle Python, les modules, exiftool, ffmpeg et ses encodeurs, et donne pour chaque élément manquant la commande à taper sur votre système.
|
La commande contrôle Python, les modules, exiftool, ffmpeg et ses encodeurs, et donne pour chaque élément manquant la commande à taper sur votre système.
|
||||||
|
|
||||||
## Organisation des dossiers
|
## Premiers pas
|
||||||
|
|
||||||
Le dépôt et le site sont deux dossiers voisins. Un voyage est un dossier qui contient au moins un des sous-dossiers `photos`, `rushs` ou `routes`. Les voyages rangés à la racine du site forment la rubrique « Voyages » de la page d'accueil. Ceux rangés un niveau plus bas, dans un dossier de regroupement (ici `Balades`), forment une rubrique du nom de ce dossier. L'accueil montre les 2 plus récents de chaque rubrique (`ACCUEIL_NB=3` pour en montrer plus), avec un lien vers la liste complète.
|
|
||||||
|
|
||||||
```
|
|
||||||
CLEROADTRIPS
|
|
||||||
|-RoadTripsGenerator ce dépôt
|
|
||||||
| |-Scripts
|
|
||||||
| |-templates mise en page (HTML, CSS, JavaScript)
|
|
||||||
|-Road-Trips le site
|
|
||||||
|-Ete-2025
|
|
||||||
| |-photos photos (JPEG)
|
|
||||||
| |-rushs vidéos d'origine (peut être un lien vers un disque externe)
|
|
||||||
| |-routes une trace GPX par jour, date dans le nom : 2025-08-02.gpx
|
|
||||||
| |-voyage.md le récit
|
|
||||||
|-Balades
|
|
||||||
|-Aisne-06-2026
|
|
||||||
|-...
|
|
||||||
```
|
|
||||||
|
|
||||||
Dans le récit `voyage.md`, deux raccourcis évitent les liens écrits en dur (qui ne marcheraient pas à la fois en local et en ligne) :
|
|
||||||
|
|
||||||
```
|
|
||||||
[Carte du jour](jour:2025-08-02)
|
|
||||||
[Le col](media:photos/IMG_4831.jpeg)
|
|
||||||
[La descente](media:videos/GH011272.mp4)
|
|
||||||
```
|
|
||||||
|
|
||||||
## L'interface
|
|
||||||
|
|
||||||
```
|
```
|
||||||
cd /Volumes/CLEROADTRIPS/Road-Trips
|
cd /Volumes/CLEROADTRIPS/Road-Trips
|
||||||
rtg interface
|
rtg interface
|
||||||
```
|
```
|
||||||
|
|
||||||
Le navigateur s'ouvre sur l'interface de RTG (adresse `http://127.0.0.1:8765/…`, visible de cet ordinateur seulement). Laisser la fenêtre de Terminal ouverte pendant l'utilisation ; Ctrl+C l'arrête.
|
L'interface s'ouvre dans le navigateur. Laisser la fenêtre de Terminal ouverte pendant l'utilisation ; Ctrl+C l'arrête.
|
||||||
|
|
||||||
- **Mes voyages** : tous les voyages par rubrique, avec leurs dates et l'avancement des vidéos, des photos et des pages. Le bouton « Nouveau voyage » crée le dossier et son `voyage.toml`.
|
- **Site existant**, organisé à l'ancienne (avant RTG) : `rtg migrer` écrit une fois `site.toml` et un `voyage.toml` par voyage.
|
||||||
- **Un voyage**, en cinq onglets :
|
- **Nouveau site** : lancer `rtg interface` dans un dossier vide ; l'interface propose de créer le site.
|
||||||
- *Journées* : la carte des traces, jour par jour, et les kilomètres. Une trace sans heures et sans date dans son nom (un itinéraire préparé, par exemple) se rattache ici à un jour. RTG propose le jour où des photos ou des vidéos ont été prises le long de la trace. On peut aussi répartir les traces à la suite, un jour par trace.
|
|
||||||
- *Carte et corrections* : glisser une photo ou une vidéo mal placée, placer celles qui n'ont pas de position (cliquer son nom puis la carte), et masquer des zones de trace : autour de chez soi, ou sur une erreur du GPS. Une zone peut valoir pour tous les voyages (zone privée). Fond plan ou satellite. Les corrections sont notées dans `voyage.toml` (`[positions]`, `[corrections]`) ou `site.toml` (`[confidentialite]`) et appliquées à la génération suivante ; les fichiers d'origine ne sont pas modifiés.
|
|
||||||
- *Sources* : où sont les traces, les vidéos et les photos d'origine, choisies dans une fenêtre de dossiers ou collées. Le nombre de fichiers trouvés s'affiche, et un disque débranché est signalé.
|
|
||||||
- *Infos et récit* : titre, moto et récit (`voyage.md`).
|
|
||||||
- *Générer* : tailles des rushs et de ce qui reste à encoder, espace libre, puis lancement de la génération ou de l'encodage seul. L'avancement de chaque vidéo s'affiche ; « Arrêter » interrompt proprement, sans perdre les vidéos déjà finies.
|
|
||||||
- **Réglages** : les réglages de `site.toml` (qualité vidéo, taille des photos, fuseau, cartes My Maps) et la vérification des outils installés.
|
|
||||||
- **Voir le site** : le site généré, servi tel quel, vidéos comprises.
|
|
||||||
|
|
||||||
### Accès à l'interface
|
**L'aide d'utilisation est dans l'interface** : un bouton « ? » sur chaque écran et chaque onglet, et la page « Aide » du menu. Elle est aussi lisible ici, dans [`rtg/interface/aide/`](rtg/interface/aide/index.md).
|
||||||
|
|
||||||
L'interface permet de modifier le site et de lire les fichiers de l'ordinateur : elle est protégée.
|
## Organisation des dossiers
|
||||||
|
|
||||||
- **Sur l'ordinateur où elle tourne**, l'adresse ouverte au lancement contient une clé d'accès tirée au hasard (`…/?jeton=…`), affichée aussi dans le Terminal. Sans elle, l'interface demande de se connecter. La clé change à chaque lancement ; le navigateur garde ensuite une session de 12 heures, perdue quand l'interface s'arrête. « Se déconnecter » la ferme tout de suite.
|
Le dépôt et le site sont deux dossiers voisins. Un voyage est un dossier qui contient un `voyage.toml` (ou, à l'ancienne, un des sous-dossiers `photos`, `rushs` ou `routes`). Les voyages rangés à la racine du site forment la rubrique « Voyages » de la page d'accueil ; ceux rangés un niveau plus bas, dans un dossier de regroupement (ici `Balades`), forment une rubrique du nom de ce dossier.
|
||||||
- **Mot de passe** : `rtg mot-de-passe` le définit (10 caractères au moins). Seule une empreinte est enregistrée, hors du site : `~/.config/rtg/acces.toml` (`%APPDATA%\rtg\acces.toml` sous Windows). `rtg mot-de-passe --supprimer` l'efface. Après 5 essais faux, l'adresse IP doit attendre une minute, puis deux, quatre… jusqu'à une heure.
|
|
||||||
- **Depuis une autre machine** (le serveur qui héberge le site, par exemple) : `rtg interface --distant`, qui exige le mot de passe. Sans HTTPS, ce mot de passe circulerait en clair. Trois solutions :
|
|
||||||
- servir l'interface derrière le serveur web du site, en HTTPS, sur une adresse à part ;
|
|
||||||
- `--certificat cert.pem --cle cle.pem` ;
|
|
||||||
- le plus simple : un tunnel SSH. Sur le serveur, lancer `rtg interface --sans-navigateur` (sans `--distant`). Sur le Mac, ouvrir `ssh -L 8765:127.0.0.1:8765 serveur`, puis l'adresse avec la clé affichée sur le serveur.
|
|
||||||
- **Bouton « Connexion » sur l'accueil du site** : voir ci-dessous.
|
|
||||||
- Les fichiers de réglages (`site.toml`, `voyage.toml`) sont dans les dossiers du site. Si le serveur web publie ce dossier tel quel, ils sont lisibles par tous, avec les chemins de tes disques. Rien de secret n'y est écrit, mais on peut les refuser au serveur web (par exemple `location ~ \.toml$ { deny all; }` pour nginx, ou `<FilesMatch "\.toml$"> Require all denied </FilesMatch>` pour Apache).
|
|
||||||
|
|
||||||
### Administration depuis le site
|
|
||||||
|
|
||||||
Pour administrer le site depuis son adresse publique (bouton « Connexion » en haut à droite de l'accueil, qui mène à `https://roadtrips.ouaibe.fr/admin/`), l'interface tourne sur la machine qui sert le site, derrière son serveur web en HTTPS. Les sources (rushs, traces, photos) doivent alors être accessibles depuis cette machine pour encoder et générer.
|
|
||||||
|
|
||||||
1. Sur cette machine, une fois : `rtg mot-de-passe`.
|
|
||||||
2. Lancer l'interface, en permanence de préférence (service, `tmux`…) :
|
|
||||||
```
|
|
||||||
rtg --racine /chemin/vers/Road-Trips interface --proxy --prefixe /admin --port 8765
|
|
||||||
```
|
|
||||||
Elle n'écoute que sur 127.0.0.1 : seul le serveur web peut la joindre. Le mot de passe est obligatoire.
|
|
||||||
3. Transmettre `/admin/` à l'interface depuis le serveur web.
|
|
||||||
- Traefik qui envoie le site à un conteneur nginx (docker compose) : rien à changer dans Traefik. Ajouter le bloc de `exemples/nginx-admin.conf` dans le `nginx.conf` du conteneur, et `extra_hosts: ["host.docker.internal:host-gateway"]` au service dans `docker-compose.yml`, puis `docker compose up -d`.
|
|
||||||
- Traefik seul (fichier de configuration dynamique) : copier `exemples/traefik-rtg-admin.yml` dans le dossier de configuration dynamique de Traefik, en y mettant les mêmes `entryPoints` et `certResolver` que pour le site. Le fichier indique l'adresse à mettre selon que Traefik tourne dans Docker sur le Mac, directement sur le Mac, ou sur une autre machine (dans ce dernier cas, lancer RTG avec `--ecoute 0.0.0.0` en plus).
|
|
||||||
- nginx, dans le bloc `server` du site en HTTPS :
|
|
||||||
```nginx
|
|
||||||
location ^~ /admin/ {
|
|
||||||
proxy_pass http://127.0.0.1:8765;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_buffering off;
|
|
||||||
}
|
|
||||||
location ~ \.toml$ { deny all; }
|
|
||||||
```
|
|
||||||
- Apache (modules `proxy`, `proxy_http`, `headers`) :
|
|
||||||
```apache
|
|
||||||
ProxyPass /admin/ http://127.0.0.1:8765/admin/
|
|
||||||
ProxyPassReverse /admin/ http://127.0.0.1:8765/admin/
|
|
||||||
RequestHeader set X-Forwarded-Proto "https"
|
|
||||||
<FilesMatch "\.toml$">
|
|
||||||
Require all denied
|
|
||||||
</FilesMatch>
|
|
||||||
```
|
|
||||||
4. Dans l'interface, Réglages → « Administration depuis le site » : adresse `/admin/`. Après « Refaire l'accueil », le bouton « Connexion » apparaît sur la page d'accueil.
|
|
||||||
|
|
||||||
### L'administration dans Docker
|
|
||||||
|
|
||||||
Plutôt que de laisser `rtg interface` ouvert dans un Terminal, on peut le faire tourner dans un conteneur, à côté du nginx du site. Le `Dockerfile` est à la racine de RoadTripsGenerator (Python, ffmpeg, exiftool).
|
|
||||||
|
|
||||||
1. Définir le mot de passe sur le Mac, une fois : `rtg mot-de-passe`, puis en copier l'empreinte à côté du `docker-compose.yml` : `mkdir -p rtg-acces && cp ~/.config/rtg/acces.toml rtg-acces/`.
|
|
||||||
2. Ajouter le service `rtg` au `docker-compose.yml` du site (voir `exemples/docker-compose.yml`) et copier `exemples/admin.sh` à côté. Le conteneur voit les disques de `/Volumes` aux mêmes chemins que sur le Mac, ce qui garde valables les sources notées dans les `voyage.toml`. Tous sont en lecture seule, sauf le dossier du site. Le disque interne (ici `MACHD`) est masqué par un dossier vide ; quant au lien `/Volumes/Macintosh HD`, il pointe dans le conteneur vers le système du conteneur, pas vers celui du Mac. Docker Desktop doit partager `/Volumes` (Réglages → Resources → File sharing) ; ce réglage permet seulement aux conteneurs de le demander, et seul `rtg` le fait.
|
|
||||||
3. Dans `nginx.conf`, faire pointer `/admin/` vers ce conteneur : voir `exemples/nginx-admin.conf`. Le site reste en ligne même si le conteneur RTG est arrêté.
|
|
||||||
4. `docker compose build rtg`, puis `docker compose up -d` (le site ; l'administration ne démarre pas avec lui).
|
|
||||||
|
|
||||||
L'administration est **à la demande** : `./admin.sh` l'ouvre, `./admin.sh stop` la ferme, et elle se ferme d'elle-même après 2 heures sans activité (variable `RTG_ARRET_INACTIF`, en minutes). Fermée, `/admin/` affiche « Administration fermée » et le site reste en ligne.
|
|
||||||
|
|
||||||
Après une mise à jour de RTG : `docker compose build rtg`. Disque des rushs branché après l'ouverture : relancer `./admin.sh`. L'encodage utilise les processeurs attribués à Docker (réglages de Docker Desktop, « Resources ») ; l'encodeur matériel VideoToolbox du Mac n'est pas disponible dans le conteneur.
|
|
||||||
|
|
||||||
Le bouton n'est qu'un lien : la protection est celle de l'interface (mot de passe, blocage après plusieurs essais faux, cookie de session limité à `/admin/`, envoyé seulement en HTTPS).
|
|
||||||
|
|
||||||
Tout ce que fait l'interface s'écrit dans `site.toml` et les `voyage.toml`. On peut toujours utiliser les commandes ci-dessous ou modifier ces fichiers à la main. Une réécriture par l'interface conserve les valeurs mais pas les commentaires.
|
|
||||||
|
|
||||||
## Utilisation : la commande `rtg`
|
|
||||||
|
|
||||||
Depuis le dossier du site (`Road-Trips`) :
|
|
||||||
|
|
||||||
```
|
```
|
||||||
rtg migrer # une fois : écrit site.toml et un voyage.toml par voyage
|
CLEROADTRIPS
|
||||||
rtg etat # avancement de chaque voyage
|
|-RoadTripsGenerator ce dépôt
|
||||||
rtg generer Balades/Vexin-10-2026 # un voyage, puis l'accueil
|
| |-rtg le logiciel (interface, pages, encodage…)
|
||||||
rtg generer --sans-compression # tous les voyages, sans encoder de vidéo
|
| |-exemples docker-compose, nginx, admin.sh
|
||||||
rtg analyser Balades/Vexin-10-2026 # ce que RTG trouve dans les sources
|
| |-Scripts anciens scripts
|
||||||
rtg verifier # tout est-il installé ?
|
|-Road-Trips le site
|
||||||
rtg interface # l'interface dans le navigateur
|
|-site.toml réglages du site
|
||||||
rtg mot-de-passe # mot de passe de l'interface (accès distant)
|
|-Ete-2025
|
||||||
rtg interface --proxy --prefixe /admin # derrière le serveur web du site (bouton Connexion)
|
| |-voyage.toml sources, infos et réglages du voyage
|
||||||
|
| |-voyage.md le récit
|
||||||
|
| |-routes une trace GPX par jour (écrite par RTG)
|
||||||
|
| |-photos, photos_web photos et versions allégées
|
||||||
|
| |-videos, apercus vidéos encodées et images d'aperçu
|
||||||
|
|-Balades
|
||||||
|
|-Aisne-06-2026
|
||||||
|
|-...
|
||||||
```
|
```
|
||||||
|
|
||||||
Les sources d'un voyage peuvent rester où elles sont, nommées n'importe comment : on les indique dans son `voyage.toml`.
|
Les sources (rushs, photos, traces d'origine) peuvent rester sur d'autres disques ; leurs chemins sont notés dans `voyage.toml`.
|
||||||
|
|
||||||
|
## Administration depuis le site : mise en place
|
||||||
|
|
||||||
|
Le bouton « Connexion » de l'accueil mène à l'interface, servie sous `/admin/` par le serveur web du site. Exemple avec le site servi par un conteneur nginx derrière Traefik, et l'interface lancée directement sur le Mac :
|
||||||
|
|
||||||
|
```
|
||||||
|
Traefik (HTTPS) → nginx (le site, dans Docker) → /admin/ → RTG sur le Mac (host.docker.internal:8765)
|
||||||
|
```
|
||||||
|
|
||||||
|
L'interface ne tourne qu'à la demande (`./admin.sh`, `./admin.sh stop`). Fermée, `/admin/` affiche « Administration fermée » et le site reste en ligne. Sur le Mac, elle voit tous les disques et encode à pleine vitesse. Son utilisation est décrite dans l'aide de l'interface.
|
||||||
|
|
||||||
|
1. **Mot de passe**, une fois : `rtg mot-de-passe` (10 caractères au moins).
|
||||||
|
2. **`docker-compose.yml`** du site : le service nginx doit pouvoir joindre le Mac, avec `extra_hosts: ["host.docker.internal:host-gateway"]`. Voir [`exemples/docker-compose.yml`](exemples/docker-compose.yml).
|
||||||
|
3. **`nginx.conf`** : ajouter le bloc `/admin/` et la page « Administration fermée », voir [`exemples/nginx-admin.conf`](exemples/nginx-admin.conf). Le `^~` de `location ^~ /admin/` est nécessaire si `nginx.conf` contient déjà une règle pour les `.js`, `.css` ou images.
|
||||||
|
4. `docker compose up -d --remove-orphans`.
|
||||||
|
5. **`admin.sh`** : copier [`exemples/admin.sh`](exemples/admin.sh) à côté du `docker-compose.yml`, puis `chmod +x admin.sh` et `./admin.sh`. L'interface tourne en arrière-plan ; son journal est dans `rtg-admin.log`.
|
||||||
|
6. Dans l'interface, **Réglages → Administration depuis le site** : `/admin/`, puis « Refaire l'accueil » pour faire apparaître le bouton « Connexion ».
|
||||||
|
|
||||||
|
Après une mise à jour de RTG : `./admin.sh stop`, puis `./admin.sh`.
|
||||||
|
|
||||||
|
**Variantes**
|
||||||
|
|
||||||
|
- **Interface dans un conteneur** : [`exemples/docker-compose-conteneur.yml`](exemples/docker-compose-conteneur.yml) et [`exemples/admin-docker.sh`](exemples/admin-docker.sh), avec le `Dockerfile` à la racine du dépôt. L'encodage y est plus lent : il n'a que les processeurs que Docker Desktop attribue, et pas l'encodeur matériel du Mac. Chaque disque des sources doit aussi être déclaré dans Docker Desktop (Réglages → Resources → File sharing). **Ne jamais déclarer `/Volumes` en entier** : avec le partage de la clé du site, Docker Desktop échoue (« mkdir /host_mnt/Volumes/…: file exists ») et nginx ne démarre plus. Le mot de passe se copie à côté du compose : `mkdir -p rtg-acces && cp ~/.config/rtg/acces.toml rtg-acces/`.
|
||||||
|
- **Traefik seul**, sans nginx : [`exemples/traefik-rtg-admin.yml`](exemples/traefik-rtg-admin.yml).
|
||||||
|
- **Autre machine du réseau** : `rtg interface --distant` (mot de passe obligatoire), de préférence en HTTPS (`--certificat`, `--cle`) ou par un tunnel SSH (`ssh -L 8765:127.0.0.1:8765 serveur`, puis l'adresse avec la clé affichée sur le serveur).
|
||||||
|
|
||||||
|
Le dossier du site étant publié tel quel, les `.toml` (chemins de vos disques) sont lisibles en ligne si le serveur web ne les refuse pas : la règle `location ~ \.toml$ { deny all; }` de l'exemple nginx s'en charge (Apache : `<FilesMatch "\.toml$"> Require all denied </FilesMatch>`). De même pour les fichiers cachés, dont `.git` si le site est sous git : règle `location ~ /\. { deny all; }`.
|
||||||
|
|
||||||
|
## Commandes
|
||||||
|
|
||||||
|
Tout ce que fait l'interface existe aussi dans le Terminal, depuis le dossier du site (`Road-Trips`) ou avec `--racine CHEMIN`.
|
||||||
|
|
||||||
|
| Commande | Rôle |
|
||||||
|
| --- | --- |
|
||||||
|
| `rtg interface` | ouvrir l'interface dans le navigateur |
|
||||||
|
| `rtg generer [VOYAGE…]` | générer un, plusieurs ou tous les voyages, puis l'accueil |
|
||||||
|
| `rtg etat` | avancement de chaque voyage |
|
||||||
|
| `rtg analyser VOYAGE` | ce que RTG trouve dans les sources d'un voyage |
|
||||||
|
| `rtg compresser VOYAGE` | encoder les vidéos d'un voyage |
|
||||||
|
| `rtg accueil` | refaire l'accueil et la carte globale |
|
||||||
|
| `rtg migrer` | une fois : écrire `site.toml` et un `voyage.toml` par voyage d'après l'existant |
|
||||||
|
| `rtg verifier` | Python, exiftool et ffmpeg sont-ils bien installés ? |
|
||||||
|
| `rtg mot-de-passe` | définir le mot de passe de l'interface (`--tester`, `--supprimer`) |
|
||||||
|
|
||||||
|
### Options utiles
|
||||||
|
|
||||||
|
- `rtg generer --sans-compression` : sans encoder les vidéos.
|
||||||
|
- `rtg generer --force` : tout refaire, même ce qui est à jour.
|
||||||
|
- `rtg generer --mymaps-maj` : récupérer à nouveau les cartes Google My Maps.
|
||||||
|
- `rtg compresser VOYAGE --reglages-changes` : réencoder aussi les vidéos faites avec d'autres réglages que les actuels.
|
||||||
|
- `rtg compresser VOYAGE --seulement 1080p` (ou `720p`) : ne traiter qu'une version.
|
||||||
|
- `rtg compresser VOYAGE --force` : tout réencoder.
|
||||||
|
- `rtg interface --port 8765 --sans-navigateur` : autre port, sans ouvrir le navigateur.
|
||||||
|
- `rtg interface --proxy --prefixe /admin` : derrière le serveur web du site (mot de passe obligatoire).
|
||||||
|
- `rtg interface --arret-inactif 120` : s'arrêter après 120 minutes sans activité.
|
||||||
|
- `rtg interface --distant [--certificat cert.pem --cle cle.pem]` : accepter les connexions du réseau, mot de passe obligatoire, HTTPS conseillé.
|
||||||
|
|
||||||
|
Exemple : `rtg compresser Balades/Aisne-06-2026 --reglages-changes --seulement 1080p`.
|
||||||
|
|
||||||
|
## Fichiers de réglages
|
||||||
|
|
||||||
|
Tout ce que fait l'interface est noté dans deux fichiers texte au format TOML. On peut aussi les modifier à la main ; une réécriture par l'interface garde les valeurs mais pas les commentaires.
|
||||||
|
|
||||||
|
### site.toml (racine du site)
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
titre = "Balade dans le Vexin"
|
titre = "Nos Road-Trips"
|
||||||
|
adresse_publique = "https://roadtrips.ouaibe.fr/"
|
||||||
|
accueil_nb_recents = 2
|
||||||
|
|
||||||
|
[video]
|
||||||
|
qualite = "equilibree" # econome | equilibree | haute
|
||||||
|
encodeur = "processeur" # processeur | materiel
|
||||||
|
preset = "medium"
|
||||||
|
jobs = 0 # 0 = automatique
|
||||||
|
|
||||||
|
[photos]
|
||||||
|
grand_cote = 1600
|
||||||
|
qualite = 80
|
||||||
|
positions_deduites = true
|
||||||
|
|
||||||
|
[journees]
|
||||||
|
fuseau = "Europe/Paris"
|
||||||
|
coupure_heure = 4
|
||||||
|
|
||||||
|
[carte_globale]
|
||||||
|
mymaps = ["https://www.google.com/maps/d/…"]
|
||||||
|
|
||||||
|
[administration]
|
||||||
|
adresse = "/admin/" # bouton Connexion sur l'accueil
|
||||||
|
|
||||||
|
[confidentialite]
|
||||||
|
zones = [[48.80, 2.10, 500]] # zones privées : latitude, longitude, rayon en m
|
||||||
|
```
|
||||||
|
|
||||||
|
### voyage.toml (un par voyage)
|
||||||
|
|
||||||
|
```toml
|
||||||
|
titre = "Été 2026"
|
||||||
moto = "BMW R 1300 GS Adventure"
|
moto = "BMW R 1300 GS Adventure"
|
||||||
|
|
||||||
[sources]
|
[sources]
|
||||||
traces = ["/Volumes/GPS/2026/vexin.gpx"]
|
traces = ["routes"]
|
||||||
videos = ["/Volumes/LaCie/Perso/Gopro/Gopro-2026/Vexin"]
|
videos = ["/Volumes/LaCie/Perso/Gopro/Gopro-2026/Ete-2026"]
|
||||||
photos = ["~/Pictures/Export/Vexin"]
|
photos = ["photos"]
|
||||||
|
|
||||||
|
[video] # réglages vidéo propres au voyage (facultatif)
|
||||||
|
qualite = "econome"
|
||||||
|
preset = "slow"
|
||||||
|
crf_hevc = 28
|
||||||
|
crf_h264 = 27
|
||||||
|
|
||||||
|
[dates_traces] # jours imposés aux traces
|
||||||
|
"Etape 3.gpx" = "2026-08-03"
|
||||||
|
|
||||||
|
[positions] # médias placés à la main
|
||||||
|
"IMG_4831.jpeg" = [48.85, 2.35]
|
||||||
|
|
||||||
|
[corrections]
|
||||||
|
masques = [[48.85, 2.35, 300]] # zones retirées des traces de ce voyage
|
||||||
```
|
```
|
||||||
|
|
||||||
RTG range alors lui-même les traces par journée (`routes/AAAA-MM-JJ.gpx`, d'après leurs horodatages, à défaut la date du nom de fichier, ou le jour choisi dans l'interface et noté dans `[dates_traces]`) et copie les photos dans `photos/` (les HEIC de l'iPhone sont converties en JPEG). Une photo sans GPS reçoit la position de la trace à son heure, quand la trace est horodatée. Les réglages communs (qualité vidéo, taille des photos, fuseau horaire, cartes My Maps…) sont dans `site.toml`.
|
Un chemin relatif (comme `routes`) part du dossier du voyage.
|
||||||
|
|
||||||
Les scripts de `Scripts/` restent disponibles pendant la transition ; ils produisent le même site. Ils demandent en plus d'installer à la main les modules Python : `pip3 install pandas folium gpxpy markdown2 numpy pillow`.
|
|
||||||
|
|
||||||
## Mode d'emploi (anciens scripts)
|
|
||||||
|
|
||||||
### Tout le site, ou quelques voyages
|
|
||||||
|
|
||||||
Depuis le dossier du site :
|
|
||||||
|
|
||||||
```
|
|
||||||
cd Road-Trips
|
|
||||||
../RoadTripsGenerator/Scripts/genere_site.sh # tous les voyages
|
|
||||||
../RoadTripsGenerator/Scripts/genere_site.sh Balades/Vexin-10-2026 # un seul
|
|
||||||
SKIP_COMPRESS=1 ../RoadTripsGenerator/Scripts/genere_site.sh # sans réencoder les vidéos
|
|
||||||
```
|
|
||||||
|
|
||||||
Pour chaque voyage, `genere_site.sh` enchaîne les étapes ci-dessous, puis régénère la page d'accueil et la carte globale. Un voyage d'une seule journée (une balade, souvent) n'a pas de carte générale intermédiaire : sa page est directement la carte du jour, avec photos et vidéos. Chaque étape ne refait que ce qui a changé : relancer le script est rapide, sauf quand de nouveaux rushs sont à encoder.
|
|
||||||
|
|
||||||
### Étape par étape
|
|
||||||
|
|
||||||
Depuis le dossier d'un voyage (ici `Road-Trips/Balades/Vexin-10-2026`, d'où les `../../../`) :
|
|
||||||
|
|
||||||
| Commande | Rôle | Produit |
|
|
||||||
|---|---|---|
|
|
||||||
| `../../../RoadTripsGenerator/Scripts/extraction_infos.sh` | positions GPS des photos et vidéos | `export_photos.csv`, `export_videos.csv` |
|
|
||||||
| `../../../RoadTripsGenerator/Scripts/compress.sh` | vidéos web (long) | `videos/` |
|
|
||||||
| `../../../RoadTripsGenerator/Scripts/prepare_web.py` | photos allégées, aperçus | `photos_web/`, `apercus/` |
|
|
||||||
| `../../../RoadTripsGenerator/Scripts/genere_carte.py` | pages du voyage | `index.html`, `mini.html`, `html/`, `assets/`, `<voyage>.kml` |
|
|
||||||
|
|
||||||
Puis, depuis le dossier du site, la page d'accueil :
|
|
||||||
|
|
||||||
```
|
|
||||||
../RoadTripsGenerator/Scripts/genere_index_general.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Sans compression, il faut faire de `videos` un lien vers `rushs` : `ln -s rushs videos`.
|
|
||||||
|
|
||||||
### Réglages utiles
|
|
||||||
|
|
||||||
* `compress.sh` : `ENCODER=vt` (encodeur matériel du Mac, bien plus rapide, un peu moins bon à taille égale), `JOBS=N`, `CRF_HEVC`, `CRF_H264`, `FORCE=1` pour tout réencoder. Une vidéo déjà encodée mais illisible (encodage interrompu) est refaite automatiquement.
|
|
||||||
* `prepare_web.py` : `WEB_PX=1600` (taille des photos), `WEB_QUALITE=80`, `FORCE=1`.
|
|
||||||
* `bench_crf.sh` : mesure la qualité (VMAF ou SSIM) d'un extrait de rush à plusieurs CRF, pour choisir `CRF_HEVC` / `CRF_H264` sur ses propres images.
|
|
||||||
|
|
||||||
### Carte globale de l'accueil
|
|
||||||
|
|
||||||
Sous les rubriques, l'accueil affiche une grande carte (`carte_globale.html`, fond OpenStreetMap, sans Google) qui réunit le contenu récupéré de nos cartes Google My Maps et les voyages du site (rubrique « Voyages »), un calque et une couleur par voyage ; un clic sur une trace mène au voyage. Un fichier `moto.txt` (une ligne) dans un voyage ajoute la moto à son nom dans la légende, par exemple « Ete 2026 (BMW R 1300 GS Adventure) ».
|
|
||||||
|
|
||||||
On peut y ajouter des cartes Google My Maps : mettre leurs liens, un par ligne, dans `mymaps.txt` à la racine du site (les cartes doivent être partagées en « Tous les utilisateurs disposant du lien »). Le KML de chaque carte est récupéré une seule fois dans `_mymaps/` ; ensuite la carte ne dépend plus de Google (`MYMAPS_MAJ=1` pour le reprendre après avoir modifié la carte Google). On peut aussi déposer à la main des exports `.kml` ou `.kmz` dans `_mymaps/`. Chaque calque My Maps devient un calque de la carte, avec ses couleurs d'origine.
|
|
||||||
|
|
||||||
Chaque voyage produit aussi un `<voyage>.kml` (tout le voyage en une seule trace, avec sa distance et un lien vers sa page, sous les 5 Mo acceptés par My Maps), pour qui voudrait l'importer dans une carte My Maps. Régler l'adresse publique du site avec `SITE_URL` (par défaut `https://roadtrip.ouaibe.fr/`) pour des liens corrects.
|
|
||||||
|
|
||||||
### Modifier l'apparence
|
|
||||||
|
|
||||||
Toute la mise en page est dans `Scripts/templates/` :
|
|
||||||
|
|
||||||
* `carte.css`, `carte.js` : style et comportement des pages de voyage (copiés dans `assets/` de chaque voyage à la génération) ;
|
|
||||||
* `fragments.html` : les morceaux de HTML (popups photo et vidéo, journal, boutons, barre des jours) ;
|
|
||||||
* `fragments_globale.html` : les popups et la légende de la carte globale ;
|
|
||||||
* `base_accueil.html`, `accueil.html`, `liste.html` : la page d'accueil (rubriques Voyages et Balades, les 2 plus récents de chacune) et les listes complètes `voyages.html`, `balades.html`.
|
|
||||||
|
|
||||||
Après une modification, `SKIP_COMPRESS=1 ../RoadTripsGenerator/Scripts/genere_site.sh` régénère tout en quelques dizaines de secondes.
|
|
||||||
|
|
||||||
## Publier
|
## Publier
|
||||||
|
|
||||||
Il suffit de copier le dossier du site. Les sous-dossiers `rushs` (originaux, très lourds) et les fichiers `export_*.csv` sont inutiles en ligne ; `photos` reste nécessaire pour l'ouverture des originaux.
|
Le site est statique. Servi directement depuis son dossier (comme ici par nginx), il est en ligne dès la génération terminée. Sinon, il suffit de le copier : les sous-dossiers `rushs` (originaux, très lourds) et les fichiers `export_*.csv` sont inutiles en ligne ; `photos` reste nécessaire pour l'ouverture des originaux.
|
||||||
|
|
||||||
|
## Anciens scripts
|
||||||
|
|
||||||
|
Les scripts de `Scripts/` (`genere_site.sh`, `compress.sh`, `prepare_web.py`…) produisent le même site que RTG ; ils restent disponibles pendant la transition. Ils demandent d'installer à la main les modules Python : `pip3 install pandas folium gpxpy markdown2 numpy pillow`. `bench_crf.sh` mesure la qualité (VMAF ou SSIM) d'un extrait de rush à plusieurs CRF, pour choisir les réglages sur ses propres images.
|
||||||
|
|
||||||
|
La mise en page de RTG est dans `rtg/templates/` (pages des voyages, accueil, carte globale) et `rtg/interface/` (interface).
|
||||||
|
|
||||||
## Contribuer
|
## Contribuer
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Variante : ouvre ou ferme l'administration RTG dans un conteneur « rtg »
|
||||||
|
# (docker-compose-conteneur.yml).
|
||||||
|
# ./admin-docker.sh ouvrir (https://roadtrips.ouaibe.fr/admin/)
|
||||||
|
# ./admin-docker.sh stop fermer
|
||||||
|
# RTG_ARRET_INACTIF (minutes, dans le compose) : fermeture automatique sans activité ; 0 = jamais.
|
||||||
|
# À placer à côté du docker-compose.yml du site.
|
||||||
|
|
||||||
|
# Disques ou dossiers des sources (rushs, photos, traces), vus en lecture seule
|
||||||
|
# aux mêmes chemins que sur le Mac. Chacun doit être déclaré dans Docker Desktop
|
||||||
|
# (Réglages → Resources → File sharing). Ceux qui sont débranchés sont ignorés.
|
||||||
|
# Avant d'éjecter l'un de ces disques : ./admin-docker.sh stop (un disque retiré sous un
|
||||||
|
# conteneur qui l'utilise peut dérégler Docker Desktop). Rebranché : ./admin-docker.sh.
|
||||||
|
SOURCES="/Volumes/LaCie /Volumes/EXTFAT"
|
||||||
|
|
||||||
|
cd "$(dirname "$0")" || exit 1
|
||||||
|
|
||||||
|
if [ "$1" = "stop" ]; then
|
||||||
|
docker compose stop rtg && echo "Administration fermée."
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Montages du jour : seulement les sources branchées, sinon Docker refuse de démarrer.
|
||||||
|
MONTAGES=""
|
||||||
|
for s in $SOURCES; do
|
||||||
|
if [ -d "$s" ]; then
|
||||||
|
MONTAGES="$MONTAGES - $s:$s:ro
|
||||||
|
"
|
||||||
|
echo "Source : $s (lecture seule)"
|
||||||
|
else
|
||||||
|
echo "Source débranchée, ignorée : $s"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
FICHIERS="-f docker-compose.yml"
|
||||||
|
if [ -n "$MONTAGES" ]; then
|
||||||
|
printf '# Écrit par admin.sh à chaque ouverture.\nservices:\n rtg:\n volumes:\n%s' "$MONTAGES" > rtg-sources.yml
|
||||||
|
FICHIERS="$FICHIERS -f rtg-sources.yml"
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker compose $FICHIERS up -d --force-recreate rtg \
|
||||||
|
&& echo "Administration ouverte : https://roadtrips.ouaibe.fr/admin/ (./admin-docker.sh stop pour la fermer)"
|
||||||
Executable → Regular
+39
-8
@@ -1,16 +1,47 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Ouvre ou ferme l'administration RTG du site.
|
# Ouvre ou ferme l'administration RTG du site, lancée directement sur le Mac.
|
||||||
# ./admin.sh ouvrir
|
# ./admin.sh ouvrir (https://roadtrips.ouaibe.fr/admin/)
|
||||||
# ./admin.sh stop fermer
|
# ./admin.sh stop fermer
|
||||||
# À placer à côté du docker-compose.yml du site.
|
# Option --arret-inactif MINUTES : fermeture automatique sans activité.
|
||||||
|
# Le nginx du site la joint par host.docker.internal:8765 (exemples/nginx-admin.conf).
|
||||||
|
# Journal : rtg-admin.log, à côté de ce script.
|
||||||
|
RACINE=/Volumes/CLEROADTRIPS/Road-Trips
|
||||||
|
RTG="" # chemin de la commande rtg si elle n'est pas trouvée toute seule
|
||||||
cd "$(dirname "$0")" || exit 1
|
cd "$(dirname "$0")" || exit 1
|
||||||
|
PID="$PWD/rtg-admin.pid"
|
||||||
|
JOURNAL="$PWD/rtg-admin.log"
|
||||||
|
|
||||||
|
ouverte() { [ -f "$PID" ] && kill -0 "$(cat "$PID")" 2>/dev/null; }
|
||||||
|
|
||||||
if [ "$1" = "stop" ]; then
|
if [ "$1" = "stop" ]; then
|
||||||
docker compose stop rtg && echo "Administration fermée."
|
if ouverte; then kill "$(cat "$PID")" && echo "Administration fermée."
|
||||||
|
else echo "L'administration n'était pas ouverte."; fi
|
||||||
|
rm -f "$PID"
|
||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[ -d /Volumes/LaCie ] || echo "LaCie débranché : les rushs ne seront pas accessibles (pas d'encodage vidéo)."
|
if ouverte; then
|
||||||
# --force-recreate : le conteneur voit les disques branchés au moment de l'ouverture.
|
echo "Déjà ouverte : https://roadtrips.ouaibe.fr/admin/"
|
||||||
docker compose up -d --force-recreate rtg \
|
exit
|
||||||
&& echo "Administration ouverte : https://roadtrips.ouaibe.fr/admin/ (se ferme seule après 2 h sans activité)"
|
fi
|
||||||
|
# rtg : celui du chemin des commandes, sinon l'emplacement habituel de pipx.
|
||||||
|
[ -n "$RTG" ] || RTG=$(command -v rtg 2>/dev/null)
|
||||||
|
for c in "$HOME/.local/bin/rtg" /opt/homebrew/bin/rtg /usr/local/bin/rtg; do
|
||||||
|
[ -n "$RTG" ] && break
|
||||||
|
[ -x "$c" ] && RTG="$c"
|
||||||
|
done
|
||||||
|
[ -n "$RTG" ] || { echo "❌ Commande rtg introuvable : tapez « type rtg » dans le Terminal et mettez son chemin dans RTG= en haut de ce script."; exit 1; }
|
||||||
|
nohup "$RTG" --racine "$RACINE" interface --proxy --prefixe /admin > "$JOURNAL" 2>&1 &
|
||||||
|
echo $! > "$PID"
|
||||||
|
# Attendre qu'elle réponde (quelques secondes au premier lancement).
|
||||||
|
i=0
|
||||||
|
while [ $i -lt 15 ] && ouverte && ! curl -s -o /dev/null http://127.0.0.1:8765/admin/connexion; do
|
||||||
|
sleep 1; i=$((i + 1))
|
||||||
|
done
|
||||||
|
if ouverte; then
|
||||||
|
echo "Administration ouverte : https://roadtrips.ouaibe.fr/admin/ (journal : $JOURNAL)"
|
||||||
|
else
|
||||||
|
echo "❌ L'administration n'a pas démarré :"
|
||||||
|
tail -5 "$JOURNAL"
|
||||||
|
rm -f "$PID"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Variante : l'administration RTG dans son propre conteneur (admin-docker.sh).
|
||||||
|
# Plus lente pour encoder (processeurs limités par Docker Desktop, pas d'encodeur
|
||||||
|
# matériel), et chaque disque des sources doit être partagé dans Docker Desktop.
|
||||||
|
# Ouvrir : ./admin-docker.sh ; fermer : ./admin-docker.sh stop.
|
||||||
|
# Après une mise à jour de RTG : docker compose build rtg
|
||||||
|
services:
|
||||||
|
roadtrip:
|
||||||
|
image: nginx:alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- ouaibe_network
|
||||||
|
volumes:
|
||||||
|
- /Volumes/CLEROADTRIPS/Road-Trips:/usr/share/nginx/html:ro
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.docker.network=ouaibe_network
|
||||||
|
- traefik.http.routers.ouaibe_roadtrip.rule=Host(`roadtrip.ouaibe.fr`) || Host(`roadtrips.ouaibe.fr`)
|
||||||
|
- traefik.http.routers.ouaibe_roadtrip.entrypoints=websecure
|
||||||
|
|
||||||
|
# Ne démarre pas avec le reste (profil « admin »). Fermeture automatique
|
||||||
|
# possible : environment: [RTG_ARRET_INACTIF=120] (minutes ; 0 = jamais, défaut).
|
||||||
|
rtg:
|
||||||
|
build: /Volumes/CLEROADTRIPS/RoadTripsGenerator
|
||||||
|
profiles: ["admin"]
|
||||||
|
restart: "no"
|
||||||
|
networks:
|
||||||
|
- ouaibe_network
|
||||||
|
volumes:
|
||||||
|
# Le site, en lecture et écriture, au même chemin que sur le Mac
|
||||||
|
# (même partage Docker Desktop que pour nginx).
|
||||||
|
- /Volumes/CLEROADTRIPS/Road-Trips:/Volumes/CLEROADTRIPS/Road-Trips
|
||||||
|
# Le mot de passe : mkdir -p rtg-acces && cp ~/.config/rtg/acces.toml rtg-acces/
|
||||||
|
- ./rtg-acces:/config/rtg:ro
|
||||||
|
# Les sources (LaCie…) sont ajoutées par admin.sh, en lecture seule.
|
||||||
|
|
||||||
|
networks:
|
||||||
|
ouaibe_network:
|
||||||
|
external: true
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
# docker-compose.yml du site, avec l'administration RTG dans son propre conteneur.
|
# docker-compose.yml du site. L'administration RTG n'y figure pas : admin.sh la
|
||||||
# Après une mise à jour de RTG : docker compose build rtg
|
# lance directement sur le Mac, à la demande (vitesse maximale, tous les disques
|
||||||
|
# visibles sans réglage) ; nginx la joint par host.docker.internal.
|
||||||
|
# Variante avec l'administration en conteneur : docker-compose-conteneur.yml.
|
||||||
services:
|
services:
|
||||||
roadtrip:
|
roadtrip:
|
||||||
image: nginx:alpine
|
image: nginx:alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
networks:
|
networks:
|
||||||
- ouaibe_network
|
- ouaibe_network
|
||||||
volumes:
|
volumes:
|
||||||
@@ -15,26 +19,6 @@ services:
|
|||||||
- traefik.http.routers.ouaibe_roadtrip.rule=Host(`roadtrip.ouaibe.fr`) || Host(`roadtrips.ouaibe.fr`)
|
- traefik.http.routers.ouaibe_roadtrip.rule=Host(`roadtrip.ouaibe.fr`) || Host(`roadtrips.ouaibe.fr`)
|
||||||
- traefik.http.routers.ouaibe_roadtrip.entrypoints=websecure
|
- traefik.http.routers.ouaibe_roadtrip.entrypoints=websecure
|
||||||
|
|
||||||
# Administration à la demande : elle ne démarre pas avec le reste.
|
|
||||||
# ouvrir : ./admin.sh fermer : ./admin.sh stop
|
|
||||||
# Mot de passe, une fois : mkdir -p rtg-acces && cp ~/.config/rtg/acces.toml rtg-acces/
|
|
||||||
# Elle se ferme d'elle-même après 2 h sans activité (RTG_ARRET_INACTIF, en minutes).
|
|
||||||
rtg:
|
|
||||||
build: /Volumes/CLEROADTRIPS/RoadTripsGenerator
|
|
||||||
profiles: ["admin"]
|
|
||||||
restart: "no"
|
|
||||||
networks:
|
|
||||||
- ouaibe_network
|
|
||||||
# Disques du Mac aux mêmes chemins que sur le Mac (les sources notées dans
|
|
||||||
# les voyage.toml restent valables), tous en lecture seule sauf le site.
|
|
||||||
# Docker Desktop doit partager /Volumes (Réglages → Resources → File sharing).
|
|
||||||
volumes:
|
|
||||||
- /Volumes:/Volumes:ro # tous les disques, lecture seule
|
|
||||||
- /Volumes/CLEROADTRIPS/Road-Trips:/Volumes/CLEROADTRIPS/Road-Trips # le site, lecture et écriture
|
|
||||||
- type: tmpfs # MACHD masqué : vide dans le conteneur
|
|
||||||
target: /Volumes/MACHD
|
|
||||||
- ./rtg-acces:/config/rtg:ro # mot de passe (copie de acces.toml)
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
ouaibe_network:
|
ouaibe_network:
|
||||||
external: true
|
external: true
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
# Bloc à ajouter dans nginx.conf, à l'intérieur de « server { … } », quand le
|
# Bloc à ajouter dans nginx.conf, à l'intérieur de « server { … } », quand le
|
||||||
# site est servi par un conteneur nginx derrière Traefik (Traefik → nginx → RTG).
|
# site est servi par un conteneur nginx derrière Traefik (Traefik → nginx → RTG).
|
||||||
|
# Si RTG ne tourne pas, /admin/ affiche « Administration fermée ».
|
||||||
|
|
||||||
# « ^~ » : sinon une règle du type « location ~* \.(js|css|png)$ » déjà
|
# « ^~ » : sinon une règle du type « location ~* \.(js|css|png)$ » déjà
|
||||||
# présente dans nginx.conf prendrait les fichiers de l'interface pour ceux du site.
|
# présente dans nginx.conf prendrait les fichiers de l'interface pour ceux du site.
|
||||||
location ^~ /admin/ {
|
location ^~ /admin/ {
|
||||||
# RTG dans le conteneur « rtg » (exemples/docker-compose.yml). Passer par
|
# RTG lancé sur le Mac par admin.sh (extra_hosts
|
||||||
# une variable laisse nginx démarrer même si ce conteneur est arrêté.
|
# « host.docker.internal:host-gateway » dans le compose).
|
||||||
resolver 127.0.0.11 valid=30s;
|
proxy_pass http://host.docker.internal:8765;
|
||||||
set $rtg http://rtg:8765;
|
# Variante, RTG dans un conteneur « rtg » (docker-compose-conteneur.yml) :
|
||||||
proxy_pass $rtg;
|
# resolver 127.0.0.11 valid=30s; set $rtg http://rtg:8765; proxy_pass $rtg;
|
||||||
# RTG lancé à la main sur le Mac à la place : remplacer les trois lignes
|
|
||||||
# ci-dessus par « proxy_pass http://host.docker.internal:8765; ».
|
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
# Traefik a déjà noté l'adresse du visiteur et le https : on les transmet tels quels.
|
# Traefik a déjà noté l'adresse du visiteur et le https : on les transmet tels quels.
|
||||||
@@ -18,7 +17,7 @@
|
|||||||
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
||||||
proxy_buffering off;
|
proxy_buffering off;
|
||||||
proxy_read_timeout 300s;
|
proxy_read_timeout 300s;
|
||||||
# Conteneur RTG arrêté : une page claire plutôt qu'une erreur 502.
|
# Administration fermée : une page claire plutôt qu'une erreur 502.
|
||||||
error_page 502 504 = @admin_fermee;
|
error_page 502 504 = @admin_fermee;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,3 +29,5 @@
|
|||||||
|
|
||||||
# Réglages de RTG : inutile de les publier.
|
# Réglages de RTG : inutile de les publier.
|
||||||
location ~ \.toml$ { deny all; }
|
location ~ \.toml$ { deny all; }
|
||||||
|
# Fichiers cachés (.git, .gitignore, .DS_Store) : jamais publiés.
|
||||||
|
location ~ /\. { deny all; }
|
||||||
+1
-1
@@ -34,4 +34,4 @@ packages = ["rtg", "rtg.interface"]
|
|||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
rtg = ["templates/*"]
|
rtg = ["templates/*"]
|
||||||
"rtg.interface" = ["modeles/*", "static/*", "static/images/*"]
|
"rtg.interface" = ["modeles/*", "static/*", "static/images/*", "aide/*"]
|
||||||
+9
-2
@@ -39,7 +39,11 @@ def main(argv=None):
|
|||||||
|
|
||||||
c = sp.add_parser("compresser", help="encoder les vidéos d'un voyage")
|
c = sp.add_parser("compresser", help="encoder les vidéos d'un voyage")
|
||||||
c.add_argument("voyage")
|
c.add_argument("voyage")
|
||||||
c.add_argument("--force", action="store_true")
|
c.add_argument("--force", action="store_true", help="refaire même les vidéos déjà encodées")
|
||||||
|
c.add_argument("--seulement", choices=["1080p", "720p"],
|
||||||
|
help="ne (re)faire qu'une des deux versions")
|
||||||
|
c.add_argument("--reglages-changes", action="store_true",
|
||||||
|
help="réencoder aussi les vidéos faites avec d'autres réglages que les actuels")
|
||||||
|
|
||||||
ac = sp.add_parser("accueil", help="régénérer l'accueil et la carte globale")
|
ac = sp.add_parser("accueil", help="régénérer l'accueil et la carte globale")
|
||||||
ac.add_argument("--mymaps-maj", action="store_true")
|
ac.add_argument("--mymaps-maj", action="store_true")
|
||||||
@@ -110,7 +114,10 @@ def main(argv=None):
|
|||||||
elif args.commande == "compresser":
|
elif args.commande == "compresser":
|
||||||
v = os.path.abspath(args.voyage)
|
v = os.path.abspath(args.voyage)
|
||||||
cfg = reglages_voyage(v, reglages_site(racine))
|
cfg = reglages_voyage(v, reglages_site(racine))
|
||||||
sys.exit(0 if video.compresser(v, cfg, sources.analyser(v, cfg), force=args.force) else 1)
|
paliers = {"1080p": ("hd",), "720p": ("sd",)}.get(args.seulement, ("hd", "sd"))
|
||||||
|
refaire = "differentes" if args.reglages_changes else "manquantes"
|
||||||
|
sys.exit(0 if video.compresser(v, cfg, sources.analyser(v, cfg), force=args.force,
|
||||||
|
paliers=paliers, refaire=refaire) else 1)
|
||||||
elif args.commande == "accueil":
|
elif args.commande == "accueil":
|
||||||
accueil.generer(racine, reglages_site(racine), args.mymaps_maj)
|
accueil.generer(racine, reglages_site(racine), args.mymaps_maj)
|
||||||
elif args.commande == "migrer":
|
elif args.commande == "migrer":
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ def parametres_video(cfg):
|
|||||||
"""Paramètres d'encodage effectifs (dictionnaire plat)."""
|
"""Paramètres d'encodage effectifs (dictionnaire plat)."""
|
||||||
v = cfg["video"]
|
v = cfg["video"]
|
||||||
q = QUALITES.get(v.get("qualite", "equilibree"), QUALITES["equilibree"])
|
q = QUALITES.get(v.get("qualite", "equilibree"), QUALITES["equilibree"])
|
||||||
|
# crf_hevc / crf_h264 dans [video] l'emportent sur la qualité choisie (essais).
|
||||||
|
q = dict(q, **{k: int(v[k]) for k in ("crf_hevc", "crf_h264") if k in v})
|
||||||
return dict(q, encodeur="vt" if v.get("encodeur") == "materiel" else "cpu",
|
return dict(q, encodeur="vt" if v.get("encodeur") == "materiel" else "cpu",
|
||||||
preset=v.get("preset", "medium"), fps_max=int(v.get("fps_max", 60)),
|
preset=v.get("preset", "medium"), fps_max=int(v.get("fps_max", 60)),
|
||||||
jobs=int(v.get("jobs", 0)), threads=int(v.get("threads", 0)))
|
jobs=int(v.get("jobs", 0)), threads=int(v.get("threads", 0)))
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Accès et sécurité
|
||||||
|
|
||||||
|
L'interface permet de modifier le site et de parcourir les disques : elle est protégée.
|
||||||
|
|
||||||
|
## Deux façons d'entrer
|
||||||
|
|
||||||
|
- **La clé d'accès** : à chaque lancement de l'interface, une clé tirée au hasard est ajoutée à l'adresse ouverte dans le navigateur (`…/?jeton=…`) et affichée dans le Terminal ou le journal. Seul celui qui la voit peut entrer. Elle change à chaque lancement.
|
||||||
|
- **Le mot de passe** : défini dans le Terminal avec `rtg mot-de-passe` (10 caractères au moins). Indispensable depuis une autre machine ou depuis le site. `rtg mot-de-passe --tester` vérifie un mot de passe ; `--supprimer` l'efface.
|
||||||
|
|
||||||
|
Le mot de passe n'est enregistré nulle part en clair : seule une empreinte est gardée, hors du site, dans `~/.config/rtg/acces.toml`.
|
||||||
|
|
||||||
|
## Session
|
||||||
|
|
||||||
|
Une fois entré, le navigateur garde une session de 12 heures après la dernière action. Elle est perdue quand l'interface s'arrête. **Se déconnecter** la ferme tout de suite.
|
||||||
|
|
||||||
|
## Protections
|
||||||
|
|
||||||
|
- Après 5 mots de passe faux, l'adresse IP doit attendre une minute, puis deux, quatre… jusqu'à une heure.
|
||||||
|
- Le cookie de session n'est lisible que par l'interface, et n'est envoyé qu'en HTTPS quand le site l'est.
|
||||||
|
- Une autre page ouverte dans le navigateur ne peut rien modifier, et l'interface refuse d'être affichée dans un cadre.
|
||||||
|
|
||||||
|
Pour l'accès depuis le site, voir [Administration depuis le site](administration.md).
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Administration depuis le site
|
||||||
|
|
||||||
|
Le bouton **Connexion** de l'accueil mène à `https://roadtrips.ouaibe.fr/admin/`. Le serveur web y fait suivre les demandes vers l'interface RTG, qui tourne directement sur le Mac :
|
||||||
|
|
||||||
|
```
|
||||||
|
Traefik (HTTPS) → nginx (le site, dans Docker) → /admin/ → RTG sur le Mac
|
||||||
|
```
|
||||||
|
|
||||||
|
L'interface ne tourne que quand on en a besoin. Fermée, `/admin/` affiche « Administration fermée » et le site reste en ligne.
|
||||||
|
|
||||||
|
## Au quotidien
|
||||||
|
|
||||||
|
Dans le dossier du `docker-compose.yml` du site :
|
||||||
|
|
||||||
|
```
|
||||||
|
./admin.sh ouvrir l'administration
|
||||||
|
./admin.sh stop la fermer
|
||||||
|
```
|
||||||
|
|
||||||
|
Elle tourne en arrière-plan : inutile de garder le Terminal ouvert. Son journal est dans `rtg-admin.log`, à côté du script. Fermer pendant un encodage l'arrête proprement ; les vidéos finies sont gardées.
|
||||||
|
|
||||||
|
À la maison, devant le Mac, on peut aussi simplement taper `rtg interface` dans le Terminal : ce sont les mêmes écrans, sans passer par le site.
|
||||||
|
|
||||||
|
## Disques et vitesse
|
||||||
|
|
||||||
|
L'interface tourne sur le Mac : elle voit tous les disques branchés, comme n'importe quelle application, et encode à pleine vitesse (tous les cœurs performance, encodeur matériel possible dans les [Réglages du site](reglages.md)).
|
||||||
|
|
||||||
|
Un disque des sources débranché est signalé dans [Sources](sources.md) ; les vidéos déjà encodées restent en ligne.
|
||||||
|
|
||||||
|
## Avant d'éjecter un disque
|
||||||
|
|
||||||
|
- **Disque de sources** : attendre la fin de l'encodage en cours, ou fermer l'administration.
|
||||||
|
- **Clé du site** : nginx s'en sert en permanence ; `docker compose stop` d'abord, `docker compose up -d` une fois rebranchée.
|
||||||
|
|
||||||
|
## Après une mise à jour de RTG
|
||||||
|
|
||||||
|
`./admin.sh stop`, puis `./admin.sh`.
|
||||||
|
|
||||||
|
## Mise en place
|
||||||
|
|
||||||
|
L'installation (nginx, mot de passe, `admin.sh`) est décrite dans le README de RoadTripsGenerator, sur git, section « Administration depuis le site : mise en place ».
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Carte et corrections
|
||||||
|
|
||||||
|
Corriger à la souris ce que les fichiers d'origine ont de faux. Les corrections sont enregistrées aussitôt dans `voyage.toml` (ou `site.toml` pour une zone privée) et appliquées **à la prochaine génération** ; les fichiers d'origine ne changent pas.
|
||||||
|
|
||||||
|
## Photos et vidéos
|
||||||
|
|
||||||
|
- **Déplacer** : glisser le marqueur (rouge pour une photo, bleu pour une vidéo). Un marqueur cerclé d'orange a une position corrigée à la main ; sa bulle propose de revenir à la position d'origine.
|
||||||
|
- **Placer un média sans position** : cliquer son nom dans « Médias sans position », puis cliquer sur la carte. Échap annule.
|
||||||
|
- Le sélecteur **Jour** et les cases **Photos** / **Vidéos** filtrent l'affichage. Le fond **Satellite** (bouton des calques) aide à placer précisément.
|
||||||
|
|
||||||
|
## Zones masquées
|
||||||
|
|
||||||
|
Les points de trace situés dans une zone sont retirés des pages : autour de chez soi, ou sur une erreur du GPS.
|
||||||
|
|
||||||
|
1. **+ Masquer une zone**, puis cliquer au centre ; un nouveau clic déplace la zone.
|
||||||
|
2. Régler le **rayon**.
|
||||||
|
3. Cocher **Zone privée** pour la retirer de **tous les voyages** (elle est alors notée dans `site.toml`).
|
||||||
|
4. **Enregistrer la zone**.
|
||||||
|
|
||||||
|
Une zone se supprime depuis la liste, bouton **Supprimer**.
|
||||||
|
|
||||||
|
Après des corrections, régénérer le voyage ([Générer](generer.md)) ; pour une zone privée, régénérer les voyages concernés.
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Générer
|
||||||
|
|
||||||
|
Fabrique tout ce que le site affiche pour ce voyage, puis refait l'accueil.
|
||||||
|
|
||||||
|
## Les étapes
|
||||||
|
|
||||||
|
1. **Sources** : les traces sont réparties par jour, les photos copiées.
|
||||||
|
2. **Positions** : dates et positions GPS des photos et des vidéos (exiftool).
|
||||||
|
3. **Vidéos** : encodage des rushs.
|
||||||
|
4. **Photos** : versions allégées pour le web, et une image d'aperçu par vidéo.
|
||||||
|
5. **Pages** : cartes du voyage et de chaque jour.
|
||||||
|
6. **Accueil** : page d'accueil, rubriques, carte globale.
|
||||||
|
|
||||||
|
Ce qui est déjà à jour n'est pas refait. Le journal s'affiche en dessous. **Arrêter** interrompt proprement : les vidéos finies sont gardées.
|
||||||
|
|
||||||
|
## Compression des vidéos
|
||||||
|
|
||||||
|
Chaque rush donne deux vidéos :
|
||||||
|
|
||||||
|
- **1080p en HEVC (H.265)**, environ 40 % plus léger que du H.264 à qualité égale ;
|
||||||
|
- **720p en H.264**, chargée par défaut et lisible partout.
|
||||||
|
|
||||||
|
Le tableau **Compression des vidéos** montre, pour chaque version :
|
||||||
|
|
||||||
|
- **Vidéos actuelles** : les réglages relus dans les fichiers eux-mêmes (les encodeurs x265 et x264 y inscrivent leurs réglages, par exemple « HEVC CRF 26 »). **conforme** ou **différent** les compare aux prochains encodages. Pour un voyage de plus de 40 vidéos, un échantillon est lu.
|
||||||
|
- **Prochains encodages** : ce que donneraient les réglages actuels.
|
||||||
|
|
||||||
|
Le **CRF** règle la qualité : plus il est élevé, plus les fichiers sont légers et moins la qualité est bonne. Les qualités du site correspondent à : Économe 28 / 27, Équilibrée 26 / 25, Haute 23 / 22 (1080p / 720p).
|
||||||
|
|
||||||
|
## Réglages vidéo propres à ce voyage
|
||||||
|
|
||||||
|
Pour tester une compression sur un seul voyage, sans toucher au reste du site : qualité, vitesse de l'encodeur (plus lent = un peu plus léger à qualité égale), ou un **CRF précis** pour le 1080p et le 720p. Laisser vide pour suivre les réglages du site. Ces réglages sont notés dans `voyage.toml`, table `[video]`.
|
||||||
|
|
||||||
|
## Quelles vidéos encoder
|
||||||
|
|
||||||
|
- **Seulement celles qui manquent** (par défaut).
|
||||||
|
- **Aussi celles faites avec d'autres réglages** : RTG relit les réglages de chaque vidéo et ne refait que celles qui diffèrent. Relancé avec les mêmes réglages, il ne refait rien.
|
||||||
|
- **Tout réencoder**.
|
||||||
|
|
||||||
|
**Versions concernées** limite le travail au **1080p** ou au **720p** ; l'autre version est gardée telle quelle. Le journal affiche pour chaque vidéo la nouvelle taille et l'ancienne (« avant … »).
|
||||||
|
|
||||||
|
**Ne pas toucher aux vidéos** saute l'encodage (utile quand le disque des rushs n'est pas branché). **Refaire aussi les photos web et les pages** les refait même si elles sont à jour.
|
||||||
|
|
||||||
|
## Boutons
|
||||||
|
|
||||||
|
- **Générer le voyage** : toutes les étapes.
|
||||||
|
- **Encoder les vidéos seulement** : l'étape Vidéos seule, avec les choix ci-dessus.
|
||||||
|
|
||||||
|
Pour essayer un réglage sur Aisne, par exemple : réglages propres au voyage, puis « aussi celles faites avec d'autres réglages », « 1080p seulement » et « Encoder les vidéos seulement ».
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Prise en main
|
||||||
|
|
||||||
|
RTG fabrique un site web de road-trips : pour chaque voyage, une carte par jour avec la trace GPS, les photos, les vidéos et le récit. Le site produit est statique : de simples fichiers, servis tels quels par le serveur web.
|
||||||
|
|
||||||
|
## Les étapes, pour un nouveau voyage
|
||||||
|
|
||||||
|
1. **Mes voyages → Nouveau voyage** : un titre, un nom de dossier, une rubrique. Voir [Mes voyages](voyages.md).
|
||||||
|
2. **Onglet Sources** : indiquer où sont les traces GPS, les vidéos et les photos d'origine. Elles restent où elles sont. Voir [Sources](sources.md).
|
||||||
|
3. **Onglet Journées** : vérifier les jours et rattacher à un jour les traces qui n'ont pas d'heures. Voir [Journées](journees.md).
|
||||||
|
4. **Onglet Générer** : lancer la génération, qui encode les vidéos, prépare les photos et fabrique les pages. Voir [Générer](generer.md).
|
||||||
|
5. **Onglets Infos et récit, Carte et corrections** : écrire le récit, corriger une photo mal placée, masquer une portion de trace, puis régénérer. Voir [Infos et récit](infos.md) et [Carte et corrections](carte.md).
|
||||||
|
|
||||||
|
Les fichiers d'origine ne sont jamais modifiés. Tout ce que fait l'interface est noté dans deux fichiers texte, `site.toml` (réglages du site) et `voyage.toml` (un par voyage). L'installation, les commandes du Terminal et le détail de ces fichiers sont dans le README de RoadTripsGenerator, sur git.
|
||||||
|
|
||||||
|
## Toutes les pages d'aide
|
||||||
|
|
||||||
|
- [Mes voyages](voyages.md) — la liste, les pastilles, créer un voyage
|
||||||
|
- [Sources](sources.md) — où RTG trouve les fichiers d'origine
|
||||||
|
- [Journées](journees.md) — les jours du voyage, les traces sans heures
|
||||||
|
- [Carte et corrections](carte.md) — déplacer une photo, masquer une zone
|
||||||
|
- [Infos et récit](infos.md) — titre, moto, récit en Markdown
|
||||||
|
- [Générer](generer.md) — génération, compression des vidéos
|
||||||
|
- [Réglages du site](reglages.md)
|
||||||
|
- [Accès et sécurité](acces.md) — clé d'accès, mot de passe
|
||||||
|
- [Administration depuis le site](administration.md) — ouvrir, fermer, disques des sources
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Infos et récit
|
||||||
|
|
||||||
|
## Titre et moto
|
||||||
|
|
||||||
|
- **Titre** : le nom du voyage sur l'accueil, dans les listes et dans la légende de la carte globale.
|
||||||
|
- **Moto** : facultative ; affichée dans la légende de la carte globale, par exemple « Ete 2026 (BMW R 1300 GS Adventure) ».
|
||||||
|
|
||||||
|
La rubrique vient du dossier où le voyage est rangé (voir [Mes voyages](voyages.md)).
|
||||||
|
|
||||||
|
## Récit
|
||||||
|
|
||||||
|
Le récit est le fichier `voyage.md`, en Markdown, affiché dans un panneau à côté des cartes du voyage.
|
||||||
|
|
||||||
|
Deux liens spéciaux, qui marchent aussi bien en local qu'en ligne :
|
||||||
|
|
||||||
|
```
|
||||||
|
[Carte du jour](jour:2026-08-01)
|
||||||
|
[Le col](media:photos/IMG_4831.jpeg)
|
||||||
|
[La descente](media:videos/GH011272.mp4)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `jour:AAAA-MM-JJ` ouvre la carte de ce jour ;
|
||||||
|
- `media:…` ouvre un média du voyage (chemin depuis le dossier du voyage).
|
||||||
|
|
||||||
|
Un lien vers un jour sans carte ou vers un média introuvable est signalé dans le journal de génération.
|
||||||
|
|
||||||
|
Enregistrer, puis régénérer le voyage pour mettre le site à jour ([Générer](generer.md)).
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Journées
|
||||||
|
|
||||||
|
Les jours du voyage d'après les traces GPS, avec leurs kilomètres, et la carte des traces (une couleur par jour).
|
||||||
|
|
||||||
|
## Traces à rattacher à un jour
|
||||||
|
|
||||||
|
Une trace **sans heures** (un itinéraire préparé, par exemple) et sans date AAAA-MM-JJ dans son nom ne peut pas être placée seule. Elle apparaît en pointillé sur la carte, dans un encadré jaune. Pour chacune :
|
||||||
|
|
||||||
|
- choisir la date puis **OK** ;
|
||||||
|
- ou accepter la **suggestion** (« Le 11 juin ? »). RTG propose le jour où des photos ou des vidéos ont été prises le long de la trace ;
|
||||||
|
- ou, pour plusieurs traces d'un coup, **Répartir** : un jour par trace, dans l'ordre des noms, à partir de la date choisie.
|
||||||
|
|
||||||
|
## Imposer un jour
|
||||||
|
|
||||||
|
Le détail « Les N fichiers de traces et leur jour » permet d'imposer un jour à n'importe quelle trace, même datée ; **Annuler** revient au jour déduit. Ce choix est noté dans `voyage.toml`, table `[dates_traces]`.
|
||||||
|
|
||||||
|
Les jours se changent à minuit, sauf avant l'heure réglée dans [Réglages du site](reglages.md) (4 h par défaut) : une étape finie à 1 h du matin reste sur la veille.
|
||||||
|
|
||||||
|
Les modifications sont prises en compte à la prochaine génération ([Générer](generer.md)).
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Réglages du site
|
||||||
|
|
||||||
|
Ils valent pour tous les voyages et sont enregistrés dans `site.toml`. Un voyage peut avoir ses propres réglages vidéo (onglet [Générer](generer.md)).
|
||||||
|
|
||||||
|
## Site
|
||||||
|
|
||||||
|
- **Titre** : affiché en haut de l'accueil.
|
||||||
|
- **Adresse publique** : l'adresse du site en ligne (par exemple `https://roadtrips.ouaibe.fr/`), pour les liens des fichiers KML.
|
||||||
|
- **Voyages récents par rubrique** : combien l'accueil en montre avant le lien « Tout voir ».
|
||||||
|
|
||||||
|
## Vidéos
|
||||||
|
|
||||||
|
- **Qualité** : Économe, Équilibrée (le réglage d'origine) ou Haute. Voir les valeurs de CRF dans [Générer](generer.md).
|
||||||
|
- **Encodeur** : **processeur** (x265 et x264, meilleure qualité à taille égale) ou **matériel** (VideoToolbox du Mac : bien plus rapide, fichiers plus gros ; indisponible dans le conteneur Docker).
|
||||||
|
- **Vitesse** : `medium` par défaut ; plus lent = un peu plus léger à qualité égale, mais plus long.
|
||||||
|
- **Vidéos encodées en parallèle** : 0 = automatique selon le nombre de cœurs.
|
||||||
|
|
||||||
|
## Photos
|
||||||
|
|
||||||
|
- **Plus grand côté** (1600 px par défaut) et **qualité JPEG** (80) des versions web. Le clic sur une photo ouvre l'original.
|
||||||
|
- **Placer les photos sans GPS d'après l'heure et la trace** : à 5 minutes près, quand la trace a des heures.
|
||||||
|
|
||||||
|
## Journées
|
||||||
|
|
||||||
|
- **Fuseau horaire** du voyage (Europe/Paris par défaut).
|
||||||
|
- **Heure de changement de jour** (4 h) : une étape finie avant cette heure reste sur la veille.
|
||||||
|
|
||||||
|
## Carte globale
|
||||||
|
|
||||||
|
Liens de cartes Google My Maps à reprendre, un par ligne. Leurs tracés sont récupérés une fois et ajoutés à la carte de l'accueil, sur fond OpenStreetMap.
|
||||||
|
|
||||||
|
## Administration depuis le site
|
||||||
|
|
||||||
|
Adresse de l'interface (par exemple `/admin/`). Si elle est remplie, l'accueil affiche un bouton « Connexion » vers cette adresse, après « Refaire l'accueil ». Voir [Administration depuis le site](administration.md).
|
||||||
|
|
||||||
|
## Système
|
||||||
|
|
||||||
|
Vérifie que Python, exiftool et ffmpeg (avec libx265 et libx264) sont installés, et l'espace libre sur le disque du site.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Sources
|
||||||
|
|
||||||
|
Où se trouvent les fichiers d'origine du voyage : traces GPS, vidéos (rushs) et photos. Ils peuvent être sur n'importe quel disque, nommés n'importe comment, et ne sont **jamais modifiés**.
|
||||||
|
|
||||||
|
## Ajouter ou retirer une source
|
||||||
|
|
||||||
|
- **Choisir un dossier…** ouvre un explorateur : naviguer, puis **Choisir ce dossier**. Le nombre de fichiers reconnus s'affiche en bas.
|
||||||
|
- Ou coller un chemin (dossier ou fichier) puis **Ajouter**.
|
||||||
|
- **Retirer** enlève la source de la liste, sans rien effacer.
|
||||||
|
|
||||||
|
Un type de source peut en avoir plusieurs (par exemple deux dossiers de photos).
|
||||||
|
|
||||||
|
## Ce que RTG en fait
|
||||||
|
|
||||||
|
- **Traces GPS** : réparties par journée et recopiées dans `routes/AAAA-MM-JJ.gpx`. Le jour vient des heures de la trace, à défaut d'une date AAAA-MM-JJ dans le nom du fichier, à défaut du jour choisi dans [Journées](journees.md).
|
||||||
|
- **Photos** : copiées dans `photos/`. Les HEIC de l'iPhone sont converties en JPEG.
|
||||||
|
- **Vidéos** : lues directement pour l'encodage, sans copie.
|
||||||
|
|
||||||
|
## Pastilles
|
||||||
|
|
||||||
|
- **N fichiers** : la source est lue.
|
||||||
|
- **introuvable** ou **disque débranché ?** : le chemin n'existe pas, ou le disque n'est pas branché. Les vidéos déjà encodées restent en ligne ; seules les nouvelles attendent le disque.
|
||||||
|
|
||||||
|
Dans la variante où l'administration tourne dans un conteneur Docker, seuls les disques déclarés dans `admin-docker.sh` sont visibles.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Mes voyages
|
||||||
|
|
||||||
|
La liste de tous les voyages du site, par rubrique, du plus récent au plus ancien.
|
||||||
|
|
||||||
|
## Rubriques
|
||||||
|
|
||||||
|
- Un voyage rangé **à la racine du site** fait partie de la rubrique **Voyages**. Ce sont aussi ceux de la carte globale de l'accueil.
|
||||||
|
- Un voyage rangé **dans un dossier** (par exemple `Balades/Aisne-06-2026`) fait partie d'une rubrique du nom de ce dossier.
|
||||||
|
|
||||||
|
## Les colonnes
|
||||||
|
|
||||||
|
| Colonne | Signification |
|
||||||
|
| --- | --- |
|
||||||
|
| Dates, Jours | d'après les traces GPS |
|
||||||
|
| Vidéos web | vidéos encodées / rushs trouvés. « sources absentes » : le disque des rushs n'est pas branché, les vidéos déjà encodées restent en ligne |
|
||||||
|
| Photos web | photos allégées pour le web / photos du voyage |
|
||||||
|
| Pages | **à jour**, **à refaire** (photos, vidéos ou récit plus récents que les pages) ou **jamais générées** |
|
||||||
|
| ⚠️ | points à vérifier (survoler pour les lire) : source introuvable, trace sans jour… |
|
||||||
|
|
||||||
|
Un clic sur le titre ouvre le voyage ; **Voir ↗** ouvre sa page sur le site.
|
||||||
|
|
||||||
|
## Boutons
|
||||||
|
|
||||||
|
- **Nouveau voyage** : un titre (affiché sur le site), un nom de dossier (dans l'adresse des pages : sans espaces ni accents, par exemple `Ete-2027`), la moto (facultative) et la rubrique. RTG crée le dossier et son `voyage.toml` ; il reste à indiquer les [Sources](sources.md).
|
||||||
|
- **Refaire l'accueil** : régénère la page d'accueil, les pages des rubriques et la carte globale, sans toucher aux voyages.
|
||||||
|
|
||||||
|
Le bandeau en haut à droite montre le traitement en cours ou le dernier terminé ; un clic ouvre son journal.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<div class="aide-texte">{{ contenu|safe }}</div>
|
||||||
|
<p class="aide-pied"><a href="{{ base }}/aide/{{ sujet }}" target="_blank">Ouvrir dans une page ↗</a> · <a href="#" hx-get="{{ base }}/aide/index?panneau=1" hx-target="#aide-contenu">Sommaire</a></p>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<div id="compression" class="encart compression" hx-get="{{ base }}/voyage/compression?v={{ v.chemin|url }}"
|
||||||
|
hx-trigger="tache-finie from:body" hx-swap="outerHTML">
|
||||||
|
<h3>Compression des vidéos</h3>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th></th><th>Vidéos actuelles</th><th>Prochains encodages</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for version in ("1080p", "720p") %}
|
||||||
|
{% set b = bilan[version] %}{% set c = cibles[version] %}
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ version }}</strong></td>
|
||||||
|
<td>
|
||||||
|
{% if not b.total %}<span class="discret">aucune</span>
|
||||||
|
{% else %}
|
||||||
|
{% for reg, n in b.reglages.most_common() %}
|
||||||
|
<div>{{ reg[0] }}{% if reg[1] is not none %} CRF {{ reg[1]|crf }}{% else %} <span class="discret">(réglages non inscrits : encodeur matériel ?)</span>{% endif %}
|
||||||
|
<span class="discret">× {{ n }}</span>
|
||||||
|
{% if reg == c %}<span class="pastille ok">conforme</span>{% else %}<span class="pastille attention">différent</span>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="discret">{{ b.total }} vidéo{{ 's' if b.total > 1 }}{% if b.lues < b.total %} · échantillon de {{ b.lues }}{% endif %}</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ c[0] }}{% if c[1] is not none %} CRF {{ c[1]|crf }}{% else %} (encodeur matériel){% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p class="aide">Qualité {{ noms_qualites.get(cfg.video.qualite, cfg.video.qualite) }}, vitesse {{ cfg.video.preset }},
|
||||||
|
encodeur {{ "matériel" if p.encodeur == "vt" else "processeur" }} :
|
||||||
|
{{ "réglages propres à ce voyage" if propre else "réglages du site" }}.
|
||||||
|
Plus le CRF est élevé, plus les fichiers sont légers, et moins la qualité est bonne.</p>
|
||||||
|
|
||||||
|
<details {{ 'open' if propre }}>
|
||||||
|
<summary>Réglages vidéo propres à ce voyage</summary>
|
||||||
|
<form class="formulaire" hx-post="{{ base }}/voyage/video" hx-target="#compression" hx-swap="outerHTML">
|
||||||
|
<input type="hidden" name="v" value="{{ v.chemin }}">
|
||||||
|
<label>Qualité
|
||||||
|
<select name="qualite">
|
||||||
|
<option value="">Comme le site ({{ noms_qualites.get(site.video.qualite, site.video.qualite) }})</option>
|
||||||
|
{% for q in qualites %}<option value="{{ q }}" {{ 'selected' if propre.qualite == q }}>{{ noms_qualites[q]|capitalize }} (CRF {{ qualites[q].crf_hevc }} / {{ qualites[q].crf_h264 }})</option>{% endfor %}
|
||||||
|
</select></label>
|
||||||
|
<label>Vitesse de l'encodeur
|
||||||
|
<select name="preset">
|
||||||
|
<option value="">Comme le site ({{ site.video.preset }})</option>
|
||||||
|
{% for pr in presets %}<option {{ 'selected' if propre.preset == pr }}>{{ pr }}</option>{% endfor %}
|
||||||
|
</select>
|
||||||
|
<span class="aide">Plus lent = un peu plus léger à qualité égale.</span></label>
|
||||||
|
<label>CRF 1080p (HEVC), pour un essai
|
||||||
|
<input type="number" name="crf_hevc" min="0" max="51" value="{{ propre.crf_hevc if propre.crf_hevc is defined }}" placeholder="selon la qualité ({{ qualites.get(cfg.video.qualite, qualites.equilibree).crf_hevc }})"></label>
|
||||||
|
<label>CRF 720p (H.264), pour un essai
|
||||||
|
<input type="number" name="crf_h264" min="0" max="51" value="{{ propre.crf_h264 if propre.crf_h264 is defined }}" placeholder="selon la qualité ({{ qualites.get(cfg.video.qualite, qualites.equilibree).crf_h264 }})"></label>
|
||||||
|
<p><button class="principal">Enregistrer</button> <span class="ok">{{ message }}</span></p>
|
||||||
|
</form>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block titre %}Aide{% endblock %}
|
||||||
|
{% block contenu %}
|
||||||
|
<div class="page-aide">
|
||||||
|
<nav class="aide-sommaire">
|
||||||
|
<h2>Aide</h2>
|
||||||
|
<ul>
|
||||||
|
{% for s, titre in sujets.items() %}
|
||||||
|
<li><a href="{{ base }}/aide/{{ s }}" class="{{ 'actif' if s == sujet }}">{{ titre }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<article class="aide-texte">{{ contenu|safe }}</article>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
<a href="{{ base }}/">Mes voyages</a>
|
<a href="{{ base }}/">Mes voyages</a>
|
||||||
<a href="{{ base }}/reglages">Réglages</a>
|
<a href="{{ base }}/reglages">Réglages</a>
|
||||||
<a href="{{ base }}/tache">Traitement</a>
|
<a href="{{ base }}/tache">Traitement</a>
|
||||||
|
<a href="{{ base }}/aide">Aide</a>
|
||||||
<a href="{{ lien_site }}" target="_blank">Voir le site ↗</a>
|
<a href="{{ lien_site }}" target="_blank">Voir le site ↗</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div id="bandeau" hx-get="{{ base }}/tache/bandeau" hx-trigger="load, every 2s"></div>
|
<div id="bandeau" hx-get="{{ base }}/tache/bandeau" hx-trigger="load, every 2s"></div>
|
||||||
@@ -25,6 +26,10 @@
|
|||||||
</main>
|
</main>
|
||||||
<div id="messages" aria-live="polite"></div>
|
<div id="messages" aria-live="polite"></div>
|
||||||
<div id="fenetre"></div>
|
<div id="fenetre"></div>
|
||||||
|
<aside id="aide" class="aide-panneau" hidden aria-label="Aide">
|
||||||
|
<button class="petit aide-fermer" data-fermer-aide title="Fermer (Échap)">✕</button>
|
||||||
|
<div id="aide-contenu"></div>
|
||||||
|
</aside>
|
||||||
<footer>RoadTripsGenerator {{ version }} · <code>{{ racine }}</code></footer>
|
<footer>RoadTripsGenerator {{ version }} · <code>{{ racine }}</code></footer>
|
||||||
<script src="{{ base }}/static/rtg.js"></script>
|
<script src="{{ base }}/static/rtg.js"></script>
|
||||||
{% block fin %}{% endblock %}
|
{% block fin %}{% endblock %}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block titre %}Nouveau voyage{% endblock %}
|
{% block titre %}Nouveau voyage{% endblock %}
|
||||||
{% block contenu %}
|
{% block contenu %}
|
||||||
<h1>Nouveau voyage</h1>
|
<h1>Nouveau voyage <button class="bouton-aide" hx-get="{{ base }}/aide/voyages?panneau=1" hx-target="#aide-contenu" data-ouvrir-aide title="Aide">?</button></h1>
|
||||||
<form id="formulaire" class="formulaire" hx-post="{{ base }}/nouveau" hx-target="#formulaire" hx-select="#formulaire" hx-swap="outerHTML">
|
<form id="formulaire" class="formulaire" hx-post="{{ base }}/nouveau" hx-target="#formulaire" hx-select="#formulaire" hx-swap="outerHTML">
|
||||||
{% if erreur %}<div class="erreur">{{ erreur }}</div>{% endif %}
|
{% if erreur %}<div class="erreur">{{ erreur }}</div>{% endif %}
|
||||||
<label>Titre
|
<label>Titre
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block titre %}Réglages{% endblock %}
|
{% block titre %}Réglages{% endblock %}
|
||||||
{% block contenu %}
|
{% block contenu %}
|
||||||
<h1>Réglages du site</h1>
|
<h1>Réglages du site <button class="bouton-aide" hx-get="{{ base }}/aide/reglages?panneau=1" hx-target="#aide-contenu" data-ouvrir-aide title="Aide">?</button></h1>
|
||||||
<p class="aide">Enregistrés dans <code>site.toml</code>. Ils valent pour tous les voyages.</p>
|
<p class="aide">Enregistrés dans <code>site.toml</code>. Ils valent pour tous les voyages.</p>
|
||||||
<form class="formulaire" hx-post="{{ base }}/reglages" hx-target="#retour-reglages">
|
<form class="formulaire" hx-post="{{ base }}/reglages" hx-target="#retour-reglages">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block titre %}Traitement{% endblock %}
|
{% block titre %}Traitement{% endblock %}
|
||||||
{% block contenu %}
|
{% block contenu %}
|
||||||
<h1>Traitement</h1>
|
<h1>Traitement <button class="bouton-aide" hx-get="{{ base }}/aide/generer?panneau=1" hx-target="#aide-contenu" data-ouvrir-aide title="Aide">?</button></h1>
|
||||||
{% if tache %}
|
{% if tache %}
|
||||||
{% if tache.voyage %}<p><a href="{{ base }}/voyage?v={{ tache.voyage|url }}">← {{ tache.voyage }}</a></p>{% endif %}
|
{% if tache.voyage %}<p><a href="{{ base }}/voyage?v={{ tache.voyage|url }}">← {{ tache.voyage }}</a></p>{% endif %}
|
||||||
{% with complet = True %}{% include "_tache.html" %}{% endwith %}
|
{% with complet = True %}{% include "_tache.html" %}{% endwith %}
|
||||||
|
|||||||
@@ -23,12 +23,14 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<section class="onglet" id="onglet-journees">
|
<section class="onglet" id="onglet-journees">
|
||||||
|
<div class="onglet-aide"><button class="bouton-aide" hx-get="{{ base }}/aide/journees?panneau=1" hx-target="#aide-contenu" data-ouvrir-aide title="Aide">? Aide</button></div>
|
||||||
<div id="analyse" hx-get="{{ base }}/voyage/analyse?v={{ v.chemin|url }}" hx-trigger="load, sources-modifiees from:body, tache-finie from:body">
|
<div id="analyse" hx-get="{{ base }}/voyage/analyse?v={{ v.chemin|url }}" hx-trigger="load, sources-modifiees from:body, tache-finie from:body">
|
||||||
<p class="chargement">Lecture des traces…</p>
|
<p class="chargement">Lecture des traces…</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="onglet" id="onglet-carte">
|
<section class="onglet" id="onglet-carte">
|
||||||
|
<div class="onglet-aide"><button class="bouton-aide" hx-get="{{ base }}/aide/carte?panneau=1" hx-target="#aide-contenu" data-ouvrir-aide title="Aide">? Aide</button></div>
|
||||||
<div id="editeur" class="editeur" data-voyage="{{ v.chemin }}">
|
<div id="editeur" class="editeur" data-voyage="{{ v.chemin }}">
|
||||||
<aside class="editeur-panneau">
|
<aside class="editeur-panneau">
|
||||||
<label>Jour
|
<label>Jour
|
||||||
@@ -64,6 +66,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="onglet" id="onglet-sources">
|
<section class="onglet" id="onglet-sources">
|
||||||
|
<div class="onglet-aide"><button class="bouton-aide" hx-get="{{ base }}/aide/sources?panneau=1" hx-target="#aide-contenu" data-ouvrir-aide title="Aide">? Aide</button></div>
|
||||||
<p class="aide">Indiquez où se trouvent les fichiers d'origine : un ou plusieurs dossiers (ou fichiers) par type, sur n'importe quel disque.
|
<p class="aide">Indiquez où se trouvent les fichiers d'origine : un ou plusieurs dossiers (ou fichiers) par type, sur n'importe quel disque.
|
||||||
RTG ne les modifie jamais : les traces sont recopiées par journée dans <code>routes/</code>, les photos dans <code>photos/</code> (HEIC converties en JPEG),
|
RTG ne les modifie jamais : les traces sont recopiées par journée dans <code>routes/</code>, les photos dans <code>photos/</code> (HEIC converties en JPEG),
|
||||||
et les vidéos sont lues directement pour l'encodage.</p>
|
et les vidéos sont lues directement pour l'encodage.</p>
|
||||||
@@ -71,6 +74,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="onglet" id="onglet-infos">
|
<section class="onglet" id="onglet-infos">
|
||||||
|
<div class="onglet-aide"><button class="bouton-aide" hx-get="{{ base }}/aide/infos?panneau=1" hx-target="#aide-contenu" data-ouvrir-aide title="Aide">? Aide</button></div>
|
||||||
<form class="formulaire" hx-post="{{ base }}/voyage/infos" hx-target="find .retour">
|
<form class="formulaire" hx-post="{{ base }}/voyage/infos" hx-target="find .retour">
|
||||||
<input type="hidden" name="v" value="{{ v.chemin }}">
|
<input type="hidden" name="v" value="{{ v.chemin }}">
|
||||||
<label>Titre <input name="titre" value="{{ cfg.titre }}"></label>
|
<label>Titre <input name="titre" value="{{ cfg.titre }}"></label>
|
||||||
@@ -90,11 +94,23 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="onglet" id="onglet-generer">
|
<section class="onglet" id="onglet-generer">
|
||||||
|
<div class="onglet-aide"><button class="bouton-aide" hx-get="{{ base }}/aide/generer?panneau=1" hx-target="#aide-contenu" data-ouvrir-aide title="Aide">? Aide</button></div>
|
||||||
<div hx-get="{{ base }}/voyage/infos-utiles?v={{ v.chemin|url }}" hx-trigger="load, sources-modifiees from:body, tache-finie from:body"><p class="chargement">Calcul des tailles…</p></div>
|
<div hx-get="{{ base }}/voyage/infos-utiles?v={{ v.chemin|url }}" hx-trigger="load, sources-modifiees from:body, tache-finie from:body"><p class="chargement">Calcul des tailles…</p></div>
|
||||||
|
<div id="compression" hx-get="{{ base }}/voyage/compression?v={{ v.chemin|url }}" hx-trigger="load, tache-finie from:body" hx-swap="outerHTML"><p class="chargement">Lecture de la compression des vidéos…</p></div>
|
||||||
<form class="formulaire" hx-post="{{ base }}/tache/lancer" hx-target="#tache-zone">
|
<form class="formulaire" hx-post="{{ base }}/tache/lancer" hx-target="#tache-zone">
|
||||||
<input type="hidden" name="v" value="{{ v.chemin }}">
|
<input type="hidden" name="v" value="{{ v.chemin }}">
|
||||||
<label class="choix"><input type="checkbox" name="sans_compression"> Sans encoder les vidéos <span class="aide">(photos, positions et pages seulement)</span></label>
|
<fieldset>
|
||||||
<label class="choix"><input type="checkbox" name="force"> Tout refaire, même ce qui est déjà à jour</label>
|
<legend>Vidéos</legend>
|
||||||
|
<label class="choix"><input type="radio" name="refaire" value="manquantes" checked> Encoder seulement celles qui manquent</label>
|
||||||
|
<label class="choix"><input type="radio" name="refaire" value="differentes"> Réencoder aussi celles faites avec d'autres réglages <span class="aide">(lus dans chaque fichier)</span></label>
|
||||||
|
<label class="choix"><input type="radio" name="refaire" value="toutes"> Tout réencoder</label>
|
||||||
|
<p class="aide">Versions concernées :
|
||||||
|
<label class="choix" style="display:inline"><input type="radio" name="versions" value="deux" checked> les deux</label>
|
||||||
|
<label class="choix" style="display:inline"><input type="radio" name="versions" value="1080p"> 1080p seulement</label>
|
||||||
|
<label class="choix" style="display:inline"><input type="radio" name="versions" value="720p"> 720p seulement</label></p>
|
||||||
|
<label class="choix"><input type="checkbox" name="sans_compression"> Ne pas toucher aux vidéos <span class="aide">(photos, positions et pages seulement)</span></label>
|
||||||
|
</fieldset>
|
||||||
|
<label class="choix"><input type="checkbox" name="force"> Refaire aussi les photos web et les pages, même à jour</label>
|
||||||
<p>
|
<p>
|
||||||
<button class="principal" name="quoi" value="generer">Générer le voyage</button>
|
<button class="principal" name="quoi" value="generer">Générer le voyage</button>
|
||||||
<button name="quoi" value="compresser">Encoder les vidéos seulement</button>
|
<button name="quoi" value="compresser">Encoder les vidéos seulement</button>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="entete-page">
|
<div class="entete-page">
|
||||||
<h1>Mes voyages <span class="compte">{{ nb }}</span></h1>
|
<h1>Mes voyages <span class="compte">{{ nb }}</span> <button class="bouton-aide" hx-get="{{ base }}/aide/voyages?panneau=1" hx-target="#aide-contenu" data-ouvrir-aide title="Aide">?</button></h1>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a class="bouton principal" href="{{ base }}/nouveau">+ Nouveau voyage</a>
|
<a class="bouton principal" href="{{ base }}/nouveau">+ Nouveau voyage</a>
|
||||||
<form hx-post="{{ base }}/tache/lancer" hx-target="#tache-zone">
|
<form hx-post="{{ base }}/tache/lancer" hx-target="#tache-zone">
|
||||||
|
|||||||
+103
-11
@@ -36,8 +36,8 @@ from urllib.parse import parse_qs, quote, unquote, urlsplit
|
|||||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||||
|
|
||||||
from rtg import __version__, acces, accueil, corrections, site, sources, video
|
from rtg import __version__, acces, accueil, corrections, site, sources, video
|
||||||
from rtg.config import (QUALITES, SITE_TOML, VOYAGE_TOML, modifier_toml, parametres_video,
|
from rtg.config import (QUALITES, SITE_TOML, VOYAGE_TOML, lire_toml, modifier_toml,
|
||||||
reglages_site, reglages_voyage)
|
parametres_video, reglages_site, reglages_voyage)
|
||||||
from rtg.interface import taches
|
from rtg.interface import taches
|
||||||
from rtg.interface.securite import Gardien
|
from rtg.interface.securite import Gardien
|
||||||
from rtg.outils import EXT_PHOTOS, EXT_VIDEOS, est_cache, lister_fichiers, taille_lisible
|
from rtg.outils import EXT_PHOTOS, EXT_VIDEOS, est_cache, lister_fichiers, taille_lisible
|
||||||
@@ -49,6 +49,7 @@ MOIS = ["janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août
|
|||||||
JOURS = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"]
|
JOURS = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"]
|
||||||
COULEURS = ["#e6194b", "#3cb44b", "#4363d8", "#f58231", "#911eb4", "#46a0b4", "#f032e6",
|
COULEURS = ["#e6194b", "#3cb44b", "#4363d8", "#f58231", "#911eb4", "#46a0b4", "#f032e6",
|
||||||
"#7a9a01", "#c47f00", "#008080", "#9a6324", "#800000", "#2f4b7c", "#808000"]
|
"#7a9a01", "#c47f00", "#008080", "#9a6324", "#800000", "#2f4b7c", "#808000"]
|
||||||
|
PRESETS = ["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower"]
|
||||||
CLES_SOURCES = {"traces": ("Traces GPS", (".gpx",), "traces GPS"),
|
CLES_SOURCES = {"traces": ("Traces GPS", (".gpx",), "traces GPS"),
|
||||||
"videos": ("Vidéos", EXT_VIDEOS, "vidéos"),
|
"videos": ("Vidéos", EXT_VIDEOS, "vidéos"),
|
||||||
"photos": ("Photos", EXT_PHOTOS, "photos")}
|
"photos": ("Photos", EXT_PHOTOS, "photos")}
|
||||||
@@ -74,6 +75,7 @@ ENV.filters["date_longue"] = _date_longue
|
|||||||
ENV.filters["date_courte"] = lambda d: f"{d.day} {MOIS[d.month - 1][:4].rstrip('.')}" if d else ""
|
ENV.filters["date_courte"] = lambda d: f"{d.day} {MOIS[d.month - 1][:4].rstrip('.')}" if d else ""
|
||||||
ENV.filters["taille"] = taille_lisible
|
ENV.filters["taille"] = taille_lisible
|
||||||
ENV.filters["url"] = lambda s: quote(str(s), safe="")
|
ENV.filters["url"] = lambda s: quote(str(s), safe="")
|
||||||
|
ENV.filters["crf"] = lambda x: f"{x:g}"
|
||||||
ENV.tests["commence_par"] = lambda s, debut: str(s).startswith(debut)
|
ENV.tests["commence_par"] = lambda s, debut: str(s).startswith(debut)
|
||||||
ENV.globals.update(version=__version__, periode=_periode, qualites=QUALITES,
|
ENV.globals.update(version=__version__, periode=_periode, qualites=QUALITES,
|
||||||
noms_qualites={"econome": "économe", "equilibree": "équilibrée", "haute": "haute"})
|
noms_qualites={"econome": "économe", "equilibree": "équilibrée", "haute": "haute"})
|
||||||
@@ -124,12 +126,15 @@ class Application:
|
|||||||
("POST", r"/voyage/dates-suite", self.voyage_dates_suite),
|
("POST", r"/voyage/dates-suite", self.voyage_dates_suite),
|
||||||
("GET", r"/voyage/infos-utiles", self.voyage_infos_utiles),
|
("GET", r"/voyage/infos-utiles", self.voyage_infos_utiles),
|
||||||
("GET", r"/voyage/carte.json", self.voyage_carte),
|
("GET", r"/voyage/carte.json", self.voyage_carte),
|
||||||
|
("GET", r"/voyage/compression", self.voyage_compression),
|
||||||
|
("POST", r"/voyage/video", self.voyage_video),
|
||||||
("POST", r"/voyage/position", self.voyage_position),
|
("POST", r"/voyage/position", self.voyage_position),
|
||||||
("POST", r"/voyage/masque", self.voyage_masque),
|
("POST", r"/voyage/masque", self.voyage_masque),
|
||||||
("GET", r"/dossiers", self.dossiers),
|
("GET", r"/dossiers", self.dossiers),
|
||||||
("GET", r"/reglages", self.reglages),
|
("GET", r"/reglages", self.reglages),
|
||||||
("POST", r"/reglages", self.reglages_enregistrer),
|
("POST", r"/reglages", self.reglages_enregistrer),
|
||||||
("GET", r"/systeme", self.systeme),
|
("GET", r"/systeme", self.systeme),
|
||||||
|
("GET", r"/aide(?:/[a-z]+)?", self.aide),
|
||||||
("POST", r"/tache/lancer", self.tache_lancer),
|
("POST", r"/tache/lancer", self.tache_lancer),
|
||||||
("POST", r"/tache/arreter", self.tache_arreter),
|
("POST", r"/tache/arreter", self.tache_arreter),
|
||||||
("GET", r"/tache", self.tache),
|
("GET", r"/tache", self.tache),
|
||||||
@@ -361,6 +366,41 @@ class Application:
|
|||||||
infos["a_encoder_octets"] = sum(os.path.getsize(f) for f in reste if os.path.exists(f))
|
infos["a_encoder_octets"] = sum(os.path.getsize(f) for f in reste if os.path.exists(f))
|
||||||
return page("_infos_utiles.html", i=infos, cfg=cfg)
|
return page("_infos_utiles.html", i=infos, cfg=cfg)
|
||||||
|
|
||||||
|
# --- Compression des vidéos
|
||||||
|
def voyage_compression(self, r, message=""):
|
||||||
|
rel = r.q("v") or r.f("v")
|
||||||
|
chemin = self.chemin_voyage(rel)
|
||||||
|
cfg = self.cfg_voyage(chemin)
|
||||||
|
site_cfg = reglages_site(self.racine)
|
||||||
|
propre = lire_toml(os.path.join(chemin, VOYAGE_TOML)).get("video", {})
|
||||||
|
p = parametres_video(cfg)
|
||||||
|
return page("_compression.html", v={"chemin": rel}, bilan=video.bilan_compression(chemin),
|
||||||
|
p=p, cibles={"1080p": video.cible("hd", p), "720p": video.cible("sd", p)},
|
||||||
|
cfg=cfg, site=site_cfg, site_p=parametres_video(site_cfg), propre=propre,
|
||||||
|
presets=PRESETS, message=message)
|
||||||
|
|
||||||
|
def voyage_video(self, r):
|
||||||
|
chemin = self.chemin_voyage(r.f("v"))
|
||||||
|
changements = {}
|
||||||
|
q = r.f("qualite")
|
||||||
|
changements["qualite"] = q if q in QUALITES else None
|
||||||
|
pr = r.f("preset")
|
||||||
|
changements["preset"] = pr if pr in PRESETS else None
|
||||||
|
for k in ("crf_hevc", "crf_h264"):
|
||||||
|
val = r.f(k).strip()
|
||||||
|
if not val:
|
||||||
|
changements[k] = None
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
n = int(val)
|
||||||
|
except ValueError:
|
||||||
|
raise Erreur(400, f"CRF invalide : {val}")
|
||||||
|
if not 0 <= n <= 51:
|
||||||
|
raise Erreur(400, "Le CRF va de 0 (sans perte) à 51")
|
||||||
|
changements[k] = n
|
||||||
|
self.toml_voyage(chemin, {"video": changements})
|
||||||
|
return self.voyage_compression(r, message="✓ Réglages vidéo du voyage enregistrés.")
|
||||||
|
|
||||||
# --- Carte et corrections
|
# --- Carte et corrections
|
||||||
def _media_json(self, chemin, rel, m):
|
def _media_json(self, chemin, rel, m):
|
||||||
nom = m["fichier"]
|
nom = m["fichier"]
|
||||||
@@ -447,6 +487,18 @@ class Application:
|
|||||||
self.toml_voyage(chemin, {"corrections": {"masques": liste}})
|
self.toml_voyage(chemin, {"corrections": {"masques": liste}})
|
||||||
return json_rep(self._masques_json(self.cfg_voyage(chemin)))
|
return json_rep(self._masques_json(self.cfg_voyage(chemin)))
|
||||||
|
|
||||||
|
# --- Aide (pages Markdown de aide/, aussi lisibles sur git)
|
||||||
|
def aide(self, r):
|
||||||
|
sujet = r.chemin.rstrip("/").rsplit("/", 1)[-1]
|
||||||
|
sujet = "index" if sujet == "aide" else sujet
|
||||||
|
panneau = r.q("panneau") == "1"
|
||||||
|
sujets = _sujets_aide()
|
||||||
|
if sujet not in sujets:
|
||||||
|
raise Erreur(404, "Page d'aide introuvable")
|
||||||
|
html = _aide_html(sujet, self.prefixe, panneau)
|
||||||
|
modele = "_aide.html" if panneau else "aide.html"
|
||||||
|
return page(modele, **self.contexte(sujet=sujet, sujets=sujets, contenu=html))
|
||||||
|
|
||||||
# --- Navigateur de dossiers
|
# --- Navigateur de dossiers
|
||||||
def dossiers(self, r):
|
def dossiers(self, r):
|
||||||
chemin = r.q("chemin") or os.path.expanduser("~")
|
chemin = r.q("chemin") or os.path.expanduser("~")
|
||||||
@@ -527,23 +579,28 @@ class Application:
|
|||||||
quoi, rel = r.f("quoi"), r.f("v")
|
quoi, rel = r.f("quoi"), r.f("v")
|
||||||
compression = r.f("sans_compression") != "on"
|
compression = r.f("sans_compression") != "on"
|
||||||
force = r.f("force") == "on"
|
force = r.f("force") == "on"
|
||||||
|
paliers = {"1080p": ("hd",), "720p": ("sd",)}.get(r.f("versions"), ("hd", "sd"))
|
||||||
|
refaire = r.f("refaire") if r.f("refaire") in video.REFAIRE else "manquantes"
|
||||||
|
nom_versions = {"1080p": " (1080p seulement)", "720p": " (720p seulement)"}.get(r.f("versions"), "")
|
||||||
racine = self.racine
|
racine = self.racine
|
||||||
if quoi in ("generer", "compresser"):
|
if quoi in ("generer", "compresser"):
|
||||||
chemin = self.chemin_voyage(rel)
|
chemin = self.chemin_voyage(rel)
|
||||||
if quoi == "generer":
|
if quoi == "generer":
|
||||||
libelle = f"Génération de {rel}" + ("" if compression else " (sans les vidéos)")
|
libelle = f"Génération de {rel}" + (nom_versions if compression else " (sans les vidéos)")
|
||||||
|
|
||||||
def fn(suivi):
|
def fn(suivi):
|
||||||
return site.generer(racine, [chemin], compression=compression, force=force, suivi=suivi)
|
return site.generer(racine, [chemin], compression=compression, force=force, suivi=suivi,
|
||||||
|
paliers=paliers, refaire=refaire, force_videos=False)
|
||||||
elif quoi == "compresser":
|
elif quoi == "compresser":
|
||||||
libelle = f"Compression des vidéos de {rel}"
|
libelle = f"Compression des vidéos de {rel}{nom_versions}"
|
||||||
|
|
||||||
def fn(suivi):
|
def fn(suivi):
|
||||||
cfg = reglages_voyage(chemin, reglages_site(racine))
|
cfg = reglages_voyage(chemin, reglages_site(racine))
|
||||||
suivi({"etape": "Vidéos", "voyage": rel})
|
suivi({"etape": "Vidéos", "voyage": rel})
|
||||||
return video.compresser(chemin, cfg, sources.analyser(chemin, cfg), force=force,
|
return video.compresser(chemin, cfg, sources.analyser(chemin, cfg), force=False,
|
||||||
progression=lambda n, e, p, rg: suivi(
|
progression=lambda n, e, p, rg: suivi(
|
||||||
{"video": n, "etat": e, "pct": p, "rang": rg}))
|
{"video": n, "etat": e, "pct": p, "rang": rg}),
|
||||||
|
paliers=paliers, refaire=refaire)
|
||||||
elif quoi == "tout":
|
elif quoi == "tout":
|
||||||
libelle = "Génération de tout le site" + ("" if compression else " (sans les vidéos)")
|
libelle = "Génération de tout le site" + ("" if compression else " (sans les vidéos)")
|
||||||
|
|
||||||
@@ -603,6 +660,36 @@ def _couches(a, jours, maxi=1500):
|
|||||||
return couches
|
return couches
|
||||||
|
|
||||||
|
|
||||||
|
AIDE = os.path.join(ICI, "aide")
|
||||||
|
|
||||||
|
|
||||||
|
def _sujets_aide():
|
||||||
|
"""{sujet: titre} des pages d'aide, dans l'ordre du sommaire (index.md)."""
|
||||||
|
titres = {}
|
||||||
|
for nom in os.listdir(AIDE):
|
||||||
|
if nom.endswith(".md"):
|
||||||
|
with open(os.path.join(AIDE, nom), encoding="utf-8") as f:
|
||||||
|
titres[nom[:-3]] = f.readline().lstrip("# ").strip()
|
||||||
|
with open(os.path.join(AIDE, "index.md"), encoding="utf-8") as f:
|
||||||
|
ordre = ["index"] + [s for s in re.findall(r"\]\(([a-z]+)\.md\)", f.read())]
|
||||||
|
ordre += sorted(set(titres) - set(ordre))
|
||||||
|
return {s: titres[s] for s in dict.fromkeys(ordre) if s in titres}
|
||||||
|
|
||||||
|
|
||||||
|
def _aide_html(sujet, base, panneau):
|
||||||
|
import markdown2
|
||||||
|
with open(os.path.join(AIDE, sujet + ".md"), encoding="utf-8") as f:
|
||||||
|
html = markdown2.markdown(f.read(), extras=["tables", "fenced-code-blocks", "cuddled-lists"])
|
||||||
|
|
||||||
|
def lien(m):
|
||||||
|
cible = m.group(1)
|
||||||
|
if panneau:
|
||||||
|
return (f'href="{base}/aide/{cible}" hx-get="{base}/aide/{cible}?panneau=1" '
|
||||||
|
f'hx-target="#aide-contenu"')
|
||||||
|
return f'href="{base}/aide/{cible}"'
|
||||||
|
return re.sub(r'href="([a-z]+)\.md"', lien, html)
|
||||||
|
|
||||||
|
|
||||||
def _valeur(d, k, defaut):
|
def _valeur(d, k, defaut):
|
||||||
v = d.get(k, defaut)
|
v = d.get(k, defaut)
|
||||||
if isinstance(v, list):
|
if isinstance(v, list):
|
||||||
@@ -611,8 +698,8 @@ def _valeur(d, k, defaut):
|
|||||||
|
|
||||||
|
|
||||||
class Requete:
|
class Requete:
|
||||||
def __init__(self, query, form):
|
def __init__(self, query, form, chemin=""):
|
||||||
self.query, self.form = query, form
|
self.query, self.form, self.chemin = query, form, chemin
|
||||||
|
|
||||||
def q(self, k, defaut=""):
|
def q(self, k, defaut=""):
|
||||||
return _valeur(self.query, k, defaut)
|
return _valeur(self.query, k, defaut)
|
||||||
@@ -727,7 +814,7 @@ def creer_gestionnaire(app, port, gardien=None):
|
|||||||
"Set-Cookie": gardien.entete_cookie("", self.https(), effacer=True)}), tete)
|
"Set-Cookie": gardien.entete_cookie("", self.https(), effacer=True)}), tete)
|
||||||
if methode == "GET" and chemin.startswith("/site/"):
|
if methode == "GET" and chemin.startswith("/site/"):
|
||||||
return self.fichier(app.racine, chemin[len("/site/"):], tete)
|
return self.fichier(app.racine, chemin[len("/site/"):], tete)
|
||||||
req = Requete(query, form)
|
req = Requete(query, form, chemin)
|
||||||
for m, motif, fn in app.routes:
|
for m, motif, fn in app.routes:
|
||||||
if m == methode and motif.match(chemin):
|
if m == methode and motif.match(chemin):
|
||||||
rep = fn(req)
|
rep = fn(req)
|
||||||
@@ -940,7 +1027,12 @@ def lancer(racine, port=8765, ouvrir=True, distant=False, certificat=None, cle=N
|
|||||||
try:
|
try:
|
||||||
serveur.serve_forever()
|
serveur.serve_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
taches.arreter()
|
# Un encodage en cours : on arrête proprement ffmpeg (sinon il continuerait
|
||||||
|
# seul et laisserait des fichiers .tmp-).
|
||||||
|
if taches.arreter():
|
||||||
|
fin = time.time() + 15
|
||||||
|
while taches.COURANTE.en_cours and time.time() < fin:
|
||||||
|
time.sleep(0.2)
|
||||||
print("\nInterface arrêtée.")
|
print("\nInterface arrêtée.")
|
||||||
finally:
|
finally:
|
||||||
serveur.server_close()
|
serveur.server_close()
|
||||||
@@ -185,3 +185,37 @@ progress.global { height: 4px; opacity: .7; }
|
|||||||
.editeur-panneau { max-height: none; }
|
.editeur-panneau { max-height: none; }
|
||||||
.editeur-carte { height: 60vh; }
|
.editeur-carte { height: 60vh; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Compression des vidéos (onglet Générer) */
|
||||||
|
.encart.compression table { background: transparent; border: none; margin-bottom: 6px; }
|
||||||
|
.encart.compression td div { margin: 2px 0; }
|
||||||
|
|
||||||
|
/* Aide */
|
||||||
|
.bouton-aide { font-size: 13px; font-weight: 700; padding: 1px 8px; border-radius: 99px; vertical-align: middle;
|
||||||
|
color: var(--accent); border-color: var(--accent); background: transparent; }
|
||||||
|
.onglet-aide { display: flex; justify-content: flex-end; margin: -6px 0 4px; }
|
||||||
|
.onglet-aide .bouton-aide { font-weight: 600; }
|
||||||
|
.aide-panneau { position: fixed; top: 0; right: 0; bottom: 0; width: min(460px, 100%); z-index: 2500;
|
||||||
|
background: var(--carte); border-left: 1px solid var(--bord); box-shadow: -8px 0 24px rgba(0,0,0,.15);
|
||||||
|
overflow-y: auto; padding: 14px 22px 30px; }
|
||||||
|
.aide-panneau[hidden] { display: none; }
|
||||||
|
.aide-fermer { position: sticky; top: 0; float: right; z-index: 1; }
|
||||||
|
.aide-texte { line-height: 1.55; }
|
||||||
|
.aide-texte h1 { font-size: 21px; margin-top: 4px; }
|
||||||
|
.aide-texte h2 { font-size: 16px; margin: 22px 0 6px; }
|
||||||
|
.aide-texte table { font-size: 13px; margin: 8px 0; }
|
||||||
|
.aide-texte th, .aide-texte td { padding: 5px 8px; }
|
||||||
|
.aide-texte pre { background: var(--code); padding: 8px 10px; border-radius: 8px; overflow-x: auto; font-size: 12px; }
|
||||||
|
.aide-texte pre code { background: none; padding: 0; }
|
||||||
|
.aide-texte li { margin: 3px 0; }
|
||||||
|
.aide-pied { border-top: 1px solid var(--bord); margin-top: 20px; padding-top: 10px; font-size: 13px; }
|
||||||
|
.page-aide { display: grid; grid-template-columns: 240px 1fr; gap: 28px; align-items: start; }
|
||||||
|
.aide-sommaire { position: sticky; top: 70px; background: var(--carte); border: 1px solid var(--bord); border-radius: 10px; padding: 4px 14px 10px; }
|
||||||
|
.aide-sommaire h2 { font-size: 15px; margin: 10px 0 6px; }
|
||||||
|
.aide-sommaire ul { list-style: none; padding: 0; margin: 0; font-size: 14px; }
|
||||||
|
.aide-sommaire li a { display: block; padding: 4px 6px; border-radius: 6px; text-decoration: none; color: var(--texte); }
|
||||||
|
.aide-sommaire li a.actif { background: var(--neutre-fond); font-weight: 600; }
|
||||||
|
.page-aide .aide-texte { background: var(--carte); border: 1px solid var(--bord); border-radius: 10px; padding: 6px 26px 20px; max-width: 760px; }
|
||||||
|
@media (max-width: 800px) { .page-aide { grid-template-columns: 1fr; } .aide-sommaire { position: static; } }
|
||||||
|
|
||||||
|
@media (min-width: 1200px) { body.aide-ouverte main, body.aide-ouverte footer { margin-right: 470px; } }
|
||||||
@@ -83,6 +83,29 @@
|
|||||||
});
|
});
|
||||||
document.addEventListener("keydown", e => { if (e.key === "Escape") fermer(); });
|
document.addEventListener("keydown", e => { if (e.key === "Escape") fermer(); });
|
||||||
|
|
||||||
|
// --- Aide contextuelle (panneau à droite) --------------------------------
|
||||||
|
function aideOuverte(oui) {
|
||||||
|
const a = document.getElementById("aide");
|
||||||
|
if (!a) return;
|
||||||
|
a.hidden = !oui;
|
||||||
|
document.body.classList.toggle("aide-ouverte", oui);
|
||||||
|
}
|
||||||
|
function fermerAide() { aideOuverte(false); }
|
||||||
|
document.addEventListener("click", e => {
|
||||||
|
if (e.target.closest("[data-ouvrir-aide]")) aideOuverte(true);
|
||||||
|
if (e.target.closest("[data-fermer-aide]")) fermerAide();
|
||||||
|
});
|
||||||
|
// Panneau ouvert : il suit l'onglet affiché.
|
||||||
|
document.addEventListener("rtg:onglet", e => {
|
||||||
|
const a = document.getElementById("aide");
|
||||||
|
const b = document.querySelector("#onglet-" + e.detail + " [data-ouvrir-aide]");
|
||||||
|
if (a && !a.hidden && b && window.htmx) htmx.ajax("GET", b.getAttribute("hx-get"), "#aide-contenu");
|
||||||
|
});
|
||||||
|
document.addEventListener("keydown", e => { if (e.key === "Escape") fermerAide(); });
|
||||||
|
document.addEventListener("htmx:afterSwap", e => {
|
||||||
|
if (e.detail.target.id === "aide-contenu") e.detail.target.parentElement.scrollTop = 0;
|
||||||
|
});
|
||||||
|
|
||||||
// Les messages (erreurs) s'effacent d'eux-mêmes.
|
// Les messages (erreurs) s'effacent d'eux-mêmes.
|
||||||
document.addEventListener("htmx:afterSwap", e => {
|
document.addEventListener("htmx:afterSwap", e => {
|
||||||
if (e.detail.target.id === "messages") {
|
if (e.detail.target.id === "messages") {
|
||||||
|
|||||||
+19
-1
@@ -83,7 +83,15 @@ def a_jour(sortie, source, force=False):
|
|||||||
|
|
||||||
def coeurs_performance():
|
def coeurs_performance():
|
||||||
"""Cœurs « performance » sur Apple Silicon (les cœurs d'efficience
|
"""Cœurs « performance » sur Apple Silicon (les cœurs d'efficience
|
||||||
ralentiraient tout le pool de threads x265), sinon tous les cœurs."""
|
ralentiraient tout le pool de threads x265), sinon les cœurs utilisables
|
||||||
|
(dans un conteneur : ceux qui lui sont attribués)."""
|
||||||
|
if hasattr(os, "sched_getaffinity") and sys.platform != "darwin":
|
||||||
|
try:
|
||||||
|
n = len(os.sched_getaffinity(0))
|
||||||
|
quota = _quota_cgroup()
|
||||||
|
return max(1, min(n, quota) if quota else n)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
if sys.platform == "darwin":
|
if sys.platform == "darwin":
|
||||||
for cle in ("hw.perflevel0.logicalcpu", "hw.logicalcpu"):
|
for cle in ("hw.perflevel0.logicalcpu", "hw.logicalcpu"):
|
||||||
r = subprocess.run(["sysctl", "-n", cle], capture_output=True, text=True)
|
r = subprocess.run(["sysctl", "-n", cle], capture_output=True, text=True)
|
||||||
@@ -92,6 +100,16 @@ def coeurs_performance():
|
|||||||
return os.cpu_count() or 4
|
return os.cpu_count() or 4
|
||||||
|
|
||||||
|
|
||||||
|
def _quota_cgroup():
|
||||||
|
"""Limite de processeurs d'un conteneur (cgroup v2 « cpu.max »), ou None."""
|
||||||
|
try:
|
||||||
|
with open("/sys/fs/cgroup/cpu.max") as f:
|
||||||
|
quota, periode = f.read().split()
|
||||||
|
return None if quota == "max" else max(1, int(int(quota) / int(periode)))
|
||||||
|
except (OSError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def taille_lisible(octets):
|
def taille_lisible(octets):
|
||||||
for unite in ("o", "Ko", "Mo", "Go"):
|
for unite in ("o", "Ko", "Mo", "Go"):
|
||||||
if octets < 1024 or unite == "Go":
|
if octets < 1024 or unite == "Go":
|
||||||
|
|||||||
+12
-5
@@ -46,8 +46,11 @@ def _sources_disponibles(voyage, liste):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def generer_voyage(voyage, racine, site_cfg, compression=True, force=False, suivi=None):
|
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
|
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."""
|
{"etape": …} au début de chaque étape et {"video": …} pendant la compression."""
|
||||||
cfg = reglages_voyage(voyage, site_cfg)
|
cfg = reglages_voyage(voyage, site_cfg)
|
||||||
nom = os.path.relpath(voyage, racine)
|
nom = os.path.relpath(voyage, racine)
|
||||||
@@ -80,7 +83,8 @@ def generer_voyage(voyage, racine, site_cfg, compression=True, force=False, suiv
|
|||||||
if suivi:
|
if suivi:
|
||||||
def progression(fichier, etat, pct, rang):
|
def progression(fichier, etat, pct, rang):
|
||||||
suivi({"video": fichier, "etat": etat, "pct": pct, "rang": rang})
|
suivi({"video": fichier, "etat": etat, "pct": pct, "rang": rang})
|
||||||
video.compresser(voyage, cfg, a, force=force, progression=progression)
|
video.compresser(voyage, cfg, a, force=force if force_videos is None else force_videos,
|
||||||
|
progression=progression, paliers=paliers, refaire=refaire)
|
||||||
elif not compression:
|
elif not compression:
|
||||||
dire("⏭️ Compression sautée.")
|
dire("⏭️ Compression sautée.")
|
||||||
|
|
||||||
@@ -92,7 +96,8 @@ def generer_voyage(voyage, racine, site_cfg, compression=True, force=False, suiv
|
|||||||
return ok
|
return ok
|
||||||
|
|
||||||
|
|
||||||
def generer(racine, voyages=None, compression=True, force=False, mymaps_maj=False, suivi=None):
|
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)
|
racine = os.path.abspath(racine)
|
||||||
cfg = reglages_site(racine)
|
cfg = reglages_site(racine)
|
||||||
liste = [os.path.abspath(v) for v in voyages] if voyages else decouvrir(racine)
|
liste = [os.path.abspath(v) for v in voyages] if voyages else decouvrir(racine)
|
||||||
@@ -100,7 +105,9 @@ def generer(racine, voyages=None, compression=True, force=False, mymaps_maj=Fals
|
|||||||
dire("❌ Aucun voyage trouvé (dossiers avec routes/, photos/, rushs/ ou voyage.toml).")
|
dire("❌ Aucun voyage trouvé (dossiers avec routes/, photos/, rushs/ ou voyage.toml).")
|
||||||
return False
|
return False
|
||||||
dire(f"📁 {len(liste)} voyage(s) : {', '.join(os.path.relpath(v, racine) for v in liste)}\n")
|
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)]
|
echecs = [v for v in liste
|
||||||
|
if not generer_voyage(v, racine, cfg, compression, force, suivi, paliers, refaire,
|
||||||
|
force_videos)]
|
||||||
verifier_arret()
|
verifier_arret()
|
||||||
if suivi:
|
if suivi:
|
||||||
suivi({"etape": "Accueil", "voyage": ""})
|
suivi({"etape": "Accueil", "voyage": ""})
|
||||||
|
|||||||
+142
-15
@@ -9,6 +9,7 @@ sorties sont écrites sous un nom caché puis renommées : un encodage
|
|||||||
interrompu ne laisse jamais un fichier abîmé sous le vrai nom.
|
interrompu ne laisse jamais un fichier abîmé sous le vrai nom.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
@@ -117,6 +118,88 @@ def sorties(voyage, src):
|
|||||||
return os.path.join(dossier, base + ".mp4"), os.path.join(dossier, base + "_mobile.mp4")
|
return os.path.join(dossier, base + ".mp4"), os.path.join(dossier, base + "_mobile.mp4")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Compression des vidéos déjà faites ------------------------------------------
|
||||||
|
# x265 et x264 inscrivent leurs réglages dans la vidéo elle-même (« options:
|
||||||
|
# … crf=26.0 … ») : on les relit au début des données, sans tout parcourir.
|
||||||
|
_MARQUES = ((b"x265 (build", "HEVC"), (b"x264 - core", "H.264"))
|
||||||
|
_cache_reglages = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _debut_des_donnees(chemin, octets=128 * 1024):
|
||||||
|
taille = os.path.getsize(chemin)
|
||||||
|
with open(chemin, "rb") as f:
|
||||||
|
pos = 0
|
||||||
|
while pos + 8 <= taille:
|
||||||
|
f.seek(pos)
|
||||||
|
h = f.read(16)
|
||||||
|
n, typ, entete = int.from_bytes(h[:4], "big"), h[4:8], 8
|
||||||
|
if n == 1:
|
||||||
|
n, entete = int.from_bytes(h[8:16], "big"), 16
|
||||||
|
elif n == 0:
|
||||||
|
n = taille - pos
|
||||||
|
if typ == b"mdat":
|
||||||
|
f.seek(pos + entete)
|
||||||
|
return f.read(octets)
|
||||||
|
if n < 8:
|
||||||
|
break
|
||||||
|
pos += n
|
||||||
|
return b""
|
||||||
|
|
||||||
|
|
||||||
|
def reglages_fichier(chemin):
|
||||||
|
"""(codec, crf) d'une vidéo encodée par RTG ; crf vaut None pour un encodage
|
||||||
|
matériel ou inconnu. Résultat gardé tant que le fichier ne change pas."""
|
||||||
|
try:
|
||||||
|
st = os.stat(chemin)
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
cle = (chemin, st.st_mtime, st.st_size)
|
||||||
|
if cle in _cache_reglages:
|
||||||
|
return _cache_reglages[cle]
|
||||||
|
res = ("autre", None)
|
||||||
|
try:
|
||||||
|
# HEVC (hvc1) : les réglages sont dans la description de la piste, au
|
||||||
|
# début du fichier ; H.264 : dans la première image, au début des données.
|
||||||
|
with open(chemin, "rb") as f:
|
||||||
|
donnees = f.read(64 * 1024)
|
||||||
|
donnees += _debut_des_donnees(chemin)
|
||||||
|
for marque, codec in _MARQUES:
|
||||||
|
i = donnees.find(marque)
|
||||||
|
if i >= 0:
|
||||||
|
texte = donnees[i:i + 6000].split(b"\x00")[0].decode("ascii", "replace")
|
||||||
|
m = re.search(r"crf=([\d.]+)", texte)
|
||||||
|
res = (codec, float(m.group(1)) if m else None)
|
||||||
|
break
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
if res[0] == "autre":
|
||||||
|
# Pas d'inscription (encodeur matériel VideoToolbox, ou autre) : le codec seul.
|
||||||
|
r = subprocess.run(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
|
||||||
|
"stream=codec_name", "-of", "csv=p=0", chemin], capture_output=True, text=True)
|
||||||
|
codec = {"hevc": "HEVC", "h264": "H.264"}.get(r.stdout.strip(), r.stdout.strip() or "inconnu")
|
||||||
|
res = (codec, None)
|
||||||
|
_cache_reglages[cle] = res
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def bilan_compression(voyage, maxi=40):
|
||||||
|
"""Compression des vidéos web du voyage, par version :
|
||||||
|
{"1080p": {"total": n, "lues": k, "reglages": Counter({(codec, crf): nb})}, "720p": …}.
|
||||||
|
Au-delà de `maxi` vidéos, un échantillon réparti sur tout le voyage est lu."""
|
||||||
|
from collections import Counter
|
||||||
|
res = {}
|
||||||
|
dossier = os.path.join(voyage, "videos")
|
||||||
|
noms = sorted(n for n in os.listdir(dossier) if n.endswith(".mp4") and not n.startswith(".")) \
|
||||||
|
if os.path.isdir(dossier) else []
|
||||||
|
for version, liste in (("1080p", [n for n in noms if not n.endswith("_mobile.mp4")]),
|
||||||
|
("720p", [n for n in noms if n.endswith("_mobile.mp4")])):
|
||||||
|
pas = max(1, -(-len(liste) // maxi))
|
||||||
|
lues = liste[::pas]
|
||||||
|
compte = Counter(r for r in (reglages_fichier(os.path.join(dossier, n)) for n in lues) if r)
|
||||||
|
res[version] = {"total": len(liste), "lues": len(lues), "reglages": compte}
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
def deja_faite(voyage, src, rapide=False):
|
def deja_faite(voyage, src, rapide=False):
|
||||||
"""Les deux sorties existent (rapide=True) ou sont lisibles par ffprobe."""
|
"""Les deux sorties existent (rapide=True) ou sont lisibles par ffprobe."""
|
||||||
hd, sd = sorties(voyage, src)
|
hd, sd = sorties(voyage, src)
|
||||||
@@ -125,10 +208,32 @@ def deja_faite(voyage, src, rapide=False):
|
|||||||
return lisible(hd) and lisible(sd)
|
return lisible(hd) and lisible(sd)
|
||||||
|
|
||||||
|
|
||||||
def compresser(voyage, cfg, analyse, force=False, progression=None):
|
REFAIRE = ("manquantes", "differentes", "toutes")
|
||||||
|
|
||||||
|
|
||||||
|
def cible(palier, p):
|
||||||
|
"""(codec, crf) que les réglages `p` donnent pour une version ; crf None
|
||||||
|
pour l'encodeur matériel, qui n'inscrit pas ses réglages."""
|
||||||
|
codec = "HEVC" if palier == "hd" else "H.264"
|
||||||
|
if p["encodeur"] == "vt":
|
||||||
|
return codec, None
|
||||||
|
return codec, float(p["crf_hevc"] if palier == "hd" else p["crf_h264"])
|
||||||
|
|
||||||
|
|
||||||
|
def compresser(voyage, cfg, analyse, force=False, progression=None, paliers=("hd", "sd"),
|
||||||
|
refaire="manquantes"):
|
||||||
"""Compresse les rushs du voyage. `progression(nom, etat, pourcentage, rang)`
|
"""Compresse les rushs du voyage. `progression(nom, etat, pourcentage, rang)`
|
||||||
est appelée à chaque changement (pour l'interface) ; rang = (i, total).
|
est appelée à chaque changement (pour l'interface) ; rang = (i, total).
|
||||||
|
|
||||||
|
refaire : « manquantes » (défaut) n'encode que les versions absentes ou
|
||||||
|
illisibles ; « differentes » réencode aussi celles dont les réglages lus
|
||||||
|
dans le fichier diffèrent des réglages actuels ; « toutes » réencode tout
|
||||||
|
(= force). `paliers` limite la reprise au 1080p ("hd",) ou au 720p ("sd",) ;
|
||||||
|
une version absente est toujours produite.
|
||||||
Renvoie True si tout a réussi."""
|
Renvoie True si tout a réussi."""
|
||||||
|
paliers = tuple(x for x in ("hd", "sd") if x in paliers) or ("hd", "sd")
|
||||||
|
if force:
|
||||||
|
refaire = "toutes"
|
||||||
videos = analyse.videos
|
videos = analyse.videos
|
||||||
os.makedirs(os.path.join(voyage, "videos"), exist_ok=True)
|
os.makedirs(os.path.join(voyage, "videos"), exist_ok=True)
|
||||||
p = parametres_video(cfg)
|
p = parametres_video(cfg)
|
||||||
@@ -142,8 +247,12 @@ def compresser(voyage, cfg, analyse, force=False, progression=None):
|
|||||||
dire(f"⚙️ Encodeur : matériel VideoToolbox (q={p['vtq_hd']} / {p['vtq_sd']})")
|
dire(f"⚙️ Encodeur : matériel VideoToolbox (q={p['vtq_hd']} / {p['vtq_sd']})")
|
||||||
else:
|
else:
|
||||||
dire(f"⚙️ Encodeur : processeur, preset={p['preset']} (HEVC CRF {p['crf_hevc']} / H.264 CRF {p['crf_h264']})")
|
dire(f"⚙️ Encodeur : processeur, preset={p['preset']} (HEVC CRF {p['crf_hevc']} / H.264 CRF {p['crf_h264']})")
|
||||||
|
dans_docker = os.path.exists("/.dockerenv")
|
||||||
dire(f"🧵 Parallélisme : {jobs} rush(s) à la fois × 2 paliers ({threads} threads en 1080p, "
|
dire(f"🧵 Parallélisme : {jobs} rush(s) à la fois × 2 paliers ({threads} threads en 1080p, "
|
||||||
f"{threads_sd} en 720p ; {coeurs} cœur(s) performance)")
|
f"{threads_sd} en 720p ; {coeurs} cœur(s) "
|
||||||
|
+ ("attribués au conteneur Docker)" if dans_docker else "performance)"))
|
||||||
|
if dans_docker and coeurs < (os.cpu_count() or coeurs):
|
||||||
|
dire(" (le conteneur est limité : voir Docker Desktop → Réglages → Resources → CPU)")
|
||||||
dire(f"📼 {len(videos)} rush(s)")
|
dire(f"📼 {len(videos)} rush(s)")
|
||||||
|
|
||||||
verrou = threading.Lock()
|
verrou = threading.Lock()
|
||||||
@@ -163,20 +272,32 @@ def compresser(voyage, cfg, analyse, force=False, progression=None):
|
|||||||
hd, sd = sorties(voyage, src)
|
hd, sd = sorties(voyage, src)
|
||||||
if ARRET.is_set():
|
if ARRET.is_set():
|
||||||
return False
|
return False
|
||||||
if not force and lisible(hd) and lisible(sd):
|
finals = {"hd": hd, "sd": sd}
|
||||||
|
manquants = [x for x in ("hd", "sd") if not lisible(finals[x])]
|
||||||
|
if refaire == "toutes":
|
||||||
|
a_faire = list(paliers)
|
||||||
|
elif refaire == "differentes":
|
||||||
|
a_faire = [x for x in paliers if x in manquants or reglages_fichier(finals[x]) != cible(x, p)]
|
||||||
|
else:
|
||||||
|
a_faire = []
|
||||||
|
a_faire = [x for x in ("hd", "sd") if x in a_faire or x in manquants]
|
||||||
|
if not a_faire:
|
||||||
with verrou:
|
with verrou:
|
||||||
bilan["sautes"] += 1
|
bilan["sautes"] += 1
|
||||||
dire(f"⏭️ {rang} {nom} — déjà faite")
|
dire(f"⏭️ {rang} {nom} — déjà faite" + (" avec ces réglages" if refaire == "differentes" else ""))
|
||||||
signaler(nom, "deja_faite", rang=(i, total))
|
signaler(nom, "deja_faite", rang=(i, total))
|
||||||
return True
|
return True
|
||||||
h, duree = _hauteur(src), _duree(src)
|
h, duree = _hauteur(src), _duree(src)
|
||||||
|
debut = time.time()
|
||||||
dire(f"🎬 {rang} {nom} ({h}p, {int(duree) // 60} min {int(duree) % 60:02d} s) — encodage…")
|
dire(f"🎬 {rang} {nom} ({h}p, {int(duree) // 60} min {int(duree) % 60:02d} s) — encodage…")
|
||||||
signaler(nom, "en_cours", 0, rang=(i, total))
|
signaler(nom, "en_cours", 0, rang=(i, total))
|
||||||
tmp_hd = os.path.join(os.path.dirname(hd), ".tmp-" + os.path.basename(hd))
|
tmp_hd = os.path.join(os.path.dirname(hd), ".tmp-" + os.path.basename(hd))
|
||||||
tmp_sd = os.path.join(os.path.dirname(sd), ".tmp-" + os.path.basename(sd))
|
tmp_sd = os.path.join(os.path.dirname(sd), ".tmp-" + os.path.basename(sd))
|
||||||
avancement = {"hd": 0.0, "sd": 0.0}
|
tmps = {"hd": tmp_hd, "sd": tmp_sd}
|
||||||
|
avancement = {x: 0.0 for x in a_faire}
|
||||||
procs, lecteurs = [], []
|
procs, lecteurs = [], []
|
||||||
for palier, tmp in (("hd", tmp_hd), ("sd", tmp_sd)):
|
for palier in a_faire:
|
||||||
|
tmp = tmps[palier]
|
||||||
pr = subprocess.Popen(_commande(src, tmp, palier, p, threads, threads_sd, h),
|
pr = subprocess.Popen(_commande(src, tmp, palier, p, threads, threads_sd, h),
|
||||||
stdout=subprocess.PIPE, text=True)
|
stdout=subprocess.PIPE, text=True)
|
||||||
th = threading.Thread(target=_suivre, args=(pr, palier, duree, avancement), daemon=True)
|
th = threading.Thread(target=_suivre, args=(pr, palier, duree, avancement), daemon=True)
|
||||||
@@ -189,10 +310,10 @@ def compresser(voyage, cfg, analyse, force=False, progression=None):
|
|||||||
if ARRET.is_set():
|
if ARRET.is_set():
|
||||||
for pr in procs:
|
for pr in procs:
|
||||||
pr.terminate()
|
pr.terminate()
|
||||||
moyenne = int((avancement["hd"] + avancement["sd"]) / 2)
|
moyenne = int(sum(avancement.values()) / len(avancement))
|
||||||
if en_direct:
|
if en_direct:
|
||||||
print(f"\r 1080p {avancement['hd']:3.0f} % · 720p {avancement['sd']:3.0f} % ",
|
print("\r " + " · ".join(f"{'1080p' if x == 'hd' else '720p'} {avancement[x]:3.0f} %"
|
||||||
end="", flush=True)
|
for x in a_faire) + " ", end="", flush=True)
|
||||||
elif 0 < moyenne < 100 and moyenne // 25 > max(dernier, 0) // 25:
|
elif 0 < moyenne < 100 and moyenne // 25 > max(dernier, 0) // 25:
|
||||||
dire(f" {nom} : {moyenne} %")
|
dire(f" {nom} : {moyenne} %")
|
||||||
if moyenne != dernier:
|
if moyenne != dernier:
|
||||||
@@ -203,15 +324,21 @@ def compresser(voyage, cfg, analyse, force=False, progression=None):
|
|||||||
for th in lecteurs:
|
for th in lecteurs:
|
||||||
th.join(timeout=5)
|
th.join(timeout=5)
|
||||||
codes = [pr.returncode for pr in procs]
|
codes = [pr.returncode for pr in procs]
|
||||||
if codes == [0, 0] and lisible(tmp_hd) and lisible(tmp_sd):
|
if all(c == 0 for c in codes) and all(lisible(tmps[x]) for x in a_faire):
|
||||||
os.replace(tmp_hd, hd)
|
avant = {x: os.path.getsize(finals[x]) for x in a_faire if os.path.exists(finals[x])}
|
||||||
os.replace(tmp_sd, sd)
|
for x in a_faire:
|
||||||
|
os.replace(tmps[x], finals[x])
|
||||||
with verrou:
|
with verrou:
|
||||||
bilan["faits"] += 1
|
bilan["faits"] += 1
|
||||||
bilan["octets_src"] += os.path.getsize(src)
|
bilan["octets_src"] += os.path.getsize(src)
|
||||||
bilan["octets_web"] += os.path.getsize(hd) + os.path.getsize(sd)
|
bilan["octets_web"] += sum(os.path.getsize(finals[x]) for x in a_faire)
|
||||||
dire(f" ✅ {nom:<24} {taille_lisible(os.path.getsize(src))} → 1080p "
|
details = " + ".join(
|
||||||
f"{taille_lisible(os.path.getsize(hd))} + 720p {taille_lisible(os.path.getsize(sd))}")
|
f"{'1080p' if x == 'hd' else '720p'} {taille_lisible(os.path.getsize(finals[x]))}"
|
||||||
|
+ (f" (avant {taille_lisible(avant[x])})" if x in avant else "") for x in a_faire)
|
||||||
|
ecoule = max(time.time() - debut, 0.001)
|
||||||
|
dire(f" ✅ {nom:<24} {taille_lisible(os.path.getsize(src))} → {details}"
|
||||||
|
f" · {int(ecoule) // 60} min {int(ecoule) % 60:02d} s"
|
||||||
|
+ (f" (×{duree / ecoule:.2f} temps réel)" if duree else ""))
|
||||||
signaler(nom, "faite")
|
signaler(nom, "faite")
|
||||||
return True
|
return True
|
||||||
for t in (tmp_hd, tmp_sd):
|
for t in (tmp_hd, tmp_sd):
|
||||||
|
|||||||
@@ -121,3 +121,28 @@ def test_deuxieme_passage_ne_refait_rien(site_test, capsys):
|
|||||||
sortie = capsys.readouterr().out
|
sortie = capsys.readouterr().out
|
||||||
assert "Aucun nouvel encodage" in sortie
|
assert "Aucun nouvel encodage" in sortie
|
||||||
assert "0 réduite(s)" in sortie
|
assert "0 réduite(s)" in sortie
|
||||||
|
|
||||||
|
|
||||||
|
def test_reencodage_selon_reglages(site_test):
|
||||||
|
"""Les réglages relus dans les vidéos ; seule la version dont les réglages
|
||||||
|
ont changé est refaite, et rien quand ils n'ont pas changé."""
|
||||||
|
from rtg import video
|
||||||
|
racine, voyage = site_test
|
||||||
|
cfg = config.reglages_voyage(str(voyage), config.reglages_site(str(racine)))
|
||||||
|
a = sources.analyser(str(voyage), cfg)
|
||||||
|
assert video.compresser(str(voyage), cfg, a)
|
||||||
|
hd, sd = video.sorties(str(voyage), a.videos[0])
|
||||||
|
assert video.reglages_fichier(hd) == ("HEVC", 26.0)
|
||||||
|
assert video.reglages_fichier(sd) == ("H.264", 25.0)
|
||||||
|
dates = (os.path.getmtime(hd), os.path.getmtime(sd))
|
||||||
|
|
||||||
|
config.modifier_toml(str(voyage / "voyage.toml"), {"video": {"crf_hevc": 30}})
|
||||||
|
cfg = config.reglages_voyage(str(voyage), config.reglages_site(str(racine)))
|
||||||
|
assert video.compresser(str(voyage), cfg, a, refaire="differentes")
|
||||||
|
assert video.reglages_fichier(hd) == ("HEVC", 30.0)
|
||||||
|
assert os.path.getmtime(sd) == dates[1] # 720p inchangé
|
||||||
|
dates = (os.path.getmtime(hd), os.path.getmtime(sd))
|
||||||
|
assert video.compresser(str(voyage), cfg, a, refaire="differentes")
|
||||||
|
assert (os.path.getmtime(hd), os.path.getmtime(sd)) == dates # rien à refaire
|
||||||
|
bilan = video.bilan_compression(str(voyage))
|
||||||
|
assert bilan["1080p"]["reglages"] == {("HEVC", 30.0): 1}
|
||||||
@@ -219,3 +219,22 @@ def test_derriere_le_serveur_web(voyage_sans_heures, tmp_path, monkeypatch):
|
|||||||
finally:
|
finally:
|
||||||
srv.shutdown()
|
srv.shutdown()
|
||||||
serveur.ENV.globals["base"] = ""
|
serveur.ENV.globals["base"] = ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_aide(client):
|
||||||
|
appel, racine, voyage = client
|
||||||
|
code, html = appel("/aide")
|
||||||
|
assert code == 200 and "Prise en main" in html and 'href="/aide/generer"' in html
|
||||||
|
code, html = appel("/aide/generer?panneau=1")
|
||||||
|
assert "<h1>Générer</h1>" in html and "CRF" in html
|
||||||
|
code, html = appel("/aide/index?panneau=1")
|
||||||
|
assert 'hx-get="/aide/sources?panneau=1"' in html # liens internes au panneau
|
||||||
|
code, html = appel("/aide/nexistepas")
|
||||||
|
assert "introuvable" in html
|
||||||
|
# Chaque lien entre pages d'aide mène à une page qui existe
|
||||||
|
import glob, re as _re
|
||||||
|
dossier = os.path.join(os.path.dirname(serveur.__file__), "aide")
|
||||||
|
sujets = {os.path.basename(f)[:-3] for f in glob.glob(os.path.join(dossier, "*.md"))}
|
||||||
|
for f in glob.glob(os.path.join(dossier, "*.md")):
|
||||||
|
for cible in _re.findall(r"\]\(([a-z]+)\.md\)", open(f, encoding="utf-8").read()):
|
||||||
|
assert cible in sujets, (f, cible)
|
||||||
Reference in new issue
Block a user