Administration native sur le Mac, réencodage sélectif et aide intégrée
- admin.sh : RTG lancé directement sur le Mac (plus de conteneur), nginx joint l'interface par host.docker.internal:8765 ; variante conteneur conservée (admin-docker.sh, docker-compose-conteneur.yml) - Compression : réglages lus dans les fichiers (CRF, codec), refaire seulement le 1080p ou le 720p, seulement si les réglages ont changé ; réglages vidéo propres à chaque voyage ; « Compression actuelle » affichée - Plus d'arrêt automatique après 2 h d'inactivité (--arret-inactif, 0 par défaut) ; arrêt propre qui attend la fin des encodages en cours - Aide contextuelle dans l'interface (rtg/interface/aide) ; README centré sur l'installation et la mise en place - Cœurs attribués au conteneur détectés (cgroup) et signalés Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mf2kATMtqVABHSYdxJMnim
This commit is contained in:
1 parent
b54ae69b19
commit
ada3d76f50
38 files changed
+1067
-264
No files matched your search
+9
-2
@@ -39,7 +39,11 @@ def main(argv=None):
|
||||
|
||||
c = sp.add_parser("compresser", help="encoder les vidéos d'un 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.add_argument("--mymaps-maj", action="store_true")
|
||||
@@ -110,7 +114,10 @@ def main(argv=None):
|
||||
elif args.commande == "compresser":
|
||||
v = os.path.abspath(args.voyage)
|
||||
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":
|
||||
accueil.generer(racine, reglages_site(racine), args.mymaps_maj)
|
||||
elif args.commande == "migrer":
|
||||
|
||||
@@ -120,6 +120,8 @@ def parametres_video(cfg):
|
||||
"""Paramètres d'encodage effectifs (dictionnaire plat)."""
|
||||
v = cfg["video"]
|
||||
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",
|
||||
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)))
|
||||
|
||||
@@ -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 }}/reglages">Réglages</a>
|
||||
<a href="{{ base }}/tache">Traitement</a>
|
||||
<a href="{{ base }}/aide">Aide</a>
|
||||
<a href="{{ lien_site }}" target="_blank">Voir le site ↗</a>
|
||||
</nav>
|
||||
<div id="bandeau" hx-get="{{ base }}/tache/bandeau" hx-trigger="load, every 2s"></div>
|
||||
@@ -25,6 +26,10 @@
|
||||
</main>
|
||||
<div id="messages" aria-live="polite"></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>
|
||||
<script src="{{ base }}/static/rtg.js"></script>
|
||||
{% block fin %}{% endblock %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% block titre %}Nouveau voyage{% endblock %}
|
||||
{% 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">
|
||||
{% if erreur %}<div class="erreur">{{ erreur }}</div>{% endif %}
|
||||
<label>Titre
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% block titre %}Réglages{% endblock %}
|
||||
{% 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>
|
||||
<form class="formulaire" hx-post="{{ base }}/reglages" hx-target="#retour-reglages">
|
||||
<fieldset>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% block titre %}Traitement{% endblock %}
|
||||
{% 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.voyage %}<p><a href="{{ base }}/voyage?v={{ tache.voyage|url }}">← {{ tache.voyage }}</a></p>{% endif %}
|
||||
{% with complet = True %}{% include "_tache.html" %}{% endwith %}
|
||||
|
||||
@@ -23,12 +23,14 @@
|
||||
</nav>
|
||||
|
||||
<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">
|
||||
<p class="chargement">Lecture des traces…</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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 }}">
|
||||
<aside class="editeur-panneau">
|
||||
<label>Jour
|
||||
@@ -64,6 +66,7 @@
|
||||
</section>
|
||||
|
||||
<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.
|
||||
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>
|
||||
@@ -71,6 +74,7 @@
|
||||
</section>
|
||||
|
||||
<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">
|
||||
<input type="hidden" name="v" value="{{ v.chemin }}">
|
||||
<label>Titre <input name="titre" value="{{ cfg.titre }}"></label>
|
||||
@@ -90,11 +94,23 @@
|
||||
</section>
|
||||
|
||||
<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 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">
|
||||
<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>
|
||||
<label class="choix"><input type="checkbox" name="force"> Tout refaire, même ce qui est déjà à jour</label>
|
||||
<fieldset>
|
||||
<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>
|
||||
<button class="principal" name="quoi" value="generer">Générer le voyage</button>
|
||||
<button name="quoi" value="compresser">Encoder les vidéos seulement</button>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
{% endif %}
|
||||
|
||||
<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">
|
||||
<a class="bouton principal" href="{{ base }}/nouveau">+ Nouveau voyage</a>
|
||||
<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 rtg import __version__, acces, accueil, corrections, site, sources, video
|
||||
from rtg.config import (QUALITES, SITE_TOML, VOYAGE_TOML, modifier_toml, parametres_video,
|
||||
reglages_site, reglages_voyage)
|
||||
from rtg.config import (QUALITES, SITE_TOML, VOYAGE_TOML, lire_toml, modifier_toml,
|
||||
parametres_video, reglages_site, reglages_voyage)
|
||||
from rtg.interface import taches
|
||||
from rtg.interface.securite import Gardien
|
||||
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"]
|
||||
COULEURS = ["#e6194b", "#3cb44b", "#4363d8", "#f58231", "#911eb4", "#46a0b4", "#f032e6",
|
||||
"#7a9a01", "#c47f00", "#008080", "#9a6324", "#800000", "#2f4b7c", "#808000"]
|
||||
PRESETS = ["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower"]
|
||||
CLES_SOURCES = {"traces": ("Traces GPS", (".gpx",), "traces GPS"),
|
||||
"videos": ("Vidéos", EXT_VIDEOS, "vidéos"),
|
||||
"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["taille"] = taille_lisible
|
||||
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.globals.update(version=__version__, periode=_periode, qualites=QUALITES,
|
||||
noms_qualites={"econome": "économe", "equilibree": "équilibrée", "haute": "haute"})
|
||||
@@ -124,12 +126,15 @@ class Application:
|
||||
("POST", r"/voyage/dates-suite", self.voyage_dates_suite),
|
||||
("GET", r"/voyage/infos-utiles", self.voyage_infos_utiles),
|
||||
("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/masque", self.voyage_masque),
|
||||
("GET", r"/dossiers", self.dossiers),
|
||||
("GET", r"/reglages", self.reglages),
|
||||
("POST", r"/reglages", self.reglages_enregistrer),
|
||||
("GET", r"/systeme", self.systeme),
|
||||
("GET", r"/aide(?:/[a-z]+)?", self.aide),
|
||||
("POST", r"/tache/lancer", self.tache_lancer),
|
||||
("POST", r"/tache/arreter", self.tache_arreter),
|
||||
("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))
|
||||
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
|
||||
def _media_json(self, chemin, rel, m):
|
||||
nom = m["fichier"]
|
||||
@@ -447,6 +487,18 @@ class Application:
|
||||
self.toml_voyage(chemin, {"corrections": {"masques": liste}})
|
||||
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
|
||||
def dossiers(self, r):
|
||||
chemin = r.q("chemin") or os.path.expanduser("~")
|
||||
@@ -527,23 +579,28 @@ class Application:
|
||||
quoi, rel = r.f("quoi"), r.f("v")
|
||||
compression = r.f("sans_compression") != "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
|
||||
if quoi in ("generer", "compresser"):
|
||||
chemin = self.chemin_voyage(rel)
|
||||
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):
|
||||
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":
|
||||
libelle = f"Compression des vidéos de {rel}"
|
||||
libelle = f"Compression des vidéos de {rel}{nom_versions}"
|
||||
|
||||
def fn(suivi):
|
||||
cfg = reglages_voyage(chemin, reglages_site(racine))
|
||||
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(
|
||||
{"video": n, "etat": e, "pct": p, "rang": rg}))
|
||||
{"video": n, "etat": e, "pct": p, "rang": rg}),
|
||||
paliers=paliers, refaire=refaire)
|
||||
elif quoi == "tout":
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
v = d.get(k, defaut)
|
||||
if isinstance(v, list):
|
||||
@@ -611,8 +698,8 @@ def _valeur(d, k, defaut):
|
||||
|
||||
|
||||
class Requete:
|
||||
def __init__(self, query, form):
|
||||
self.query, self.form = query, form
|
||||
def __init__(self, query, form, chemin=""):
|
||||
self.query, self.form, self.chemin = query, form, chemin
|
||||
|
||||
def q(self, 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)
|
||||
if methode == "GET" and chemin.startswith("/site/"):
|
||||
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:
|
||||
if m == methode and motif.match(chemin):
|
||||
rep = fn(req)
|
||||
@@ -940,7 +1027,12 @@ def lancer(racine, port=8765, ouvrir=True, distant=False, certificat=None, cle=N
|
||||
try:
|
||||
serveur.serve_forever()
|
||||
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.")
|
||||
finally:
|
||||
serveur.server_close()
|
||||
@@ -185,3 +185,37 @@ progress.global { height: 4px; opacity: .7; }
|
||||
.editeur-panneau { max-height: none; }
|
||||
.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(); });
|
||||
|
||||
// --- 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.
|
||||
document.addEventListener("htmx:afterSwap", e => {
|
||||
if (e.detail.target.id === "messages") {
|
||||
|
||||
+19
-1
@@ -83,7 +83,15 @@ def a_jour(sortie, source, force=False):
|
||||
|
||||
def coeurs_performance():
|
||||
"""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":
|
||||
for cle in ("hw.perflevel0.logicalcpu", "hw.logicalcpu"):
|
||||
r = subprocess.run(["sysctl", "-n", cle], capture_output=True, text=True)
|
||||
@@ -92,6 +100,16 @@ def coeurs_performance():
|
||||
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):
|
||||
for unite in ("o", "Ko", "Mo", "Go"):
|
||||
if octets < 1024 or unite == "Go":
|
||||
|
||||
+12
-5
@@ -46,8 +46,11 @@ def _sources_disponibles(voyage, liste):
|
||||
return True
|
||||
|
||||
|
||||
def generer_voyage(voyage, racine, site_cfg, compression=True, force=False, suivi=None):
|
||||
"""Toute la chaîne pour un voyage. `suivi(evenement)` (interface) reçoit
|
||||
def generer_voyage(voyage, racine, site_cfg, compression=True, force=False, suivi=None,
|
||||
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."""
|
||||
cfg = reglages_voyage(voyage, site_cfg)
|
||||
nom = os.path.relpath(voyage, racine)
|
||||
@@ -80,7 +83,8 @@ def generer_voyage(voyage, racine, site_cfg, compression=True, force=False, suiv
|
||||
if suivi:
|
||||
def progression(fichier, etat, pct, rang):
|
||||
suivi({"video": fichier, "etat": etat, "pct": pct, "rang": rang})
|
||||
video.compresser(voyage, cfg, a, force=force, progression=progression)
|
||||
video.compresser(voyage, cfg, a, force=force if force_videos is None else force_videos,
|
||||
progression=progression, paliers=paliers, refaire=refaire)
|
||||
elif not compression:
|
||||
dire("⏭️ Compression sautée.")
|
||||
|
||||
@@ -92,7 +96,8 @@ def generer_voyage(voyage, racine, site_cfg, compression=True, force=False, suiv
|
||||
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)
|
||||
cfg = reglages_site(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).")
|
||||
return False
|
||||
dire(f"📁 {len(liste)} voyage(s) : {', '.join(os.path.relpath(v, racine) for v in liste)}\n")
|
||||
echecs = [v for v in liste if not generer_voyage(v, racine, cfg, compression, force, suivi)]
|
||||
echecs = [v for v in liste
|
||||
if not generer_voyage(v, racine, cfg, compression, force, suivi, paliers, refaire,
|
||||
force_videos)]
|
||||
verifier_arret()
|
||||
if suivi:
|
||||
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.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
@@ -117,6 +118,88 @@ def sorties(voyage, src):
|
||||
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):
|
||||
"""Les deux sorties existent (rapide=True) ou sont lisibles par ffprobe."""
|
||||
hd, sd = sorties(voyage, src)
|
||||
@@ -125,10 +208,32 @@ def deja_faite(voyage, src, rapide=False):
|
||||
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)`
|
||||
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."""
|
||||
paliers = tuple(x for x in ("hd", "sd") if x in paliers) or ("hd", "sd")
|
||||
if force:
|
||||
refaire = "toutes"
|
||||
videos = analyse.videos
|
||||
os.makedirs(os.path.join(voyage, "videos"), exist_ok=True)
|
||||
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']})")
|
||||
else:
|
||||
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, "
|
||||
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)")
|
||||
|
||||
verrou = threading.Lock()
|
||||
@@ -163,20 +272,32 @@ def compresser(voyage, cfg, analyse, force=False, progression=None):
|
||||
hd, sd = sorties(voyage, src)
|
||||
if ARRET.is_set():
|
||||
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:
|
||||
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))
|
||||
return True
|
||||
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…")
|
||||
signaler(nom, "en_cours", 0, rang=(i, total))
|
||||
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))
|
||||
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 = [], []
|
||||
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),
|
||||
stdout=subprocess.PIPE, text=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():
|
||||
for pr in procs:
|
||||
pr.terminate()
|
||||
moyenne = int((avancement["hd"] + avancement["sd"]) / 2)
|
||||
moyenne = int(sum(avancement.values()) / len(avancement))
|
||||
if en_direct:
|
||||
print(f"\r 1080p {avancement['hd']:3.0f} % · 720p {avancement['sd']:3.0f} % ",
|
||||
end="", flush=True)
|
||||
print("\r " + " · ".join(f"{'1080p' if x == 'hd' else '720p'} {avancement[x]:3.0f} %"
|
||||
for x in a_faire) + " ", end="", flush=True)
|
||||
elif 0 < moyenne < 100 and moyenne // 25 > max(dernier, 0) // 25:
|
||||
dire(f" {nom} : {moyenne} %")
|
||||
if moyenne != dernier:
|
||||
@@ -203,15 +324,21 @@ def compresser(voyage, cfg, analyse, force=False, progression=None):
|
||||
for th in lecteurs:
|
||||
th.join(timeout=5)
|
||||
codes = [pr.returncode for pr in procs]
|
||||
if codes == [0, 0] and lisible(tmp_hd) and lisible(tmp_sd):
|
||||
os.replace(tmp_hd, hd)
|
||||
os.replace(tmp_sd, sd)
|
||||
if all(c == 0 for c in codes) and all(lisible(tmps[x]) for x in a_faire):
|
||||
avant = {x: os.path.getsize(finals[x]) for x in a_faire if os.path.exists(finals[x])}
|
||||
for x in a_faire:
|
||||
os.replace(tmps[x], finals[x])
|
||||
with verrou:
|
||||
bilan["faits"] += 1
|
||||
bilan["octets_src"] += os.path.getsize(src)
|
||||
bilan["octets_web"] += os.path.getsize(hd) + os.path.getsize(sd)
|
||||
dire(f" ✅ {nom:<24} {taille_lisible(os.path.getsize(src))} → 1080p "
|
||||
f"{taille_lisible(os.path.getsize(hd))} + 720p {taille_lisible(os.path.getsize(sd))}")
|
||||
bilan["octets_web"] += sum(os.path.getsize(finals[x]) for x in a_faire)
|
||||
details = " + ".join(
|
||||
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")
|
||||
return True
|
||||
for t in (tmp_hd, tmp_sd):
|
||||
|
||||
Reference in new issue
Block a user