From 596797d2aabfdf77f5a6f594081a4d1942feb86e Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Thu, 15 Oct 2026 01:45:08 +0200 Subject: [PATCH 1/2] docs: add the facts the old French pages had and the English lost, and a Language section --- README.md | 12 ++++++++++++ docs/architecture.md | 4 ++-- docs/filebot-parity.md | 4 +++- docs/metadata-sources.md | 17 +++++++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f1a5b4e..eda50a5 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,18 @@ already sets it, and removes every key from `config.toml`. - [docs/filebot-parity.md](docs/filebot-parity.md): which FileBot features KayaBot has today and which it does not. +## Language + +This README and the pages above are also in French. **English is canonical**: +when a translation disagrees with it, the English file is right. + +- **Français** - [README](docs/i18n/fr/README.md) · [architecture](docs/i18n/fr/docs/architecture.md) · [metadata sources](docs/i18n/fr/docs/metadata-sources.md) · [FileBot parity](docs/i18n/fr/docs/filebot-parity.md) + +Each translation carries the hash of the English file it was made from, and CI +fails when the English moves ahead: see [`scripts/i18n-check.sh`](scripts/i18n-check.sh). +`docs/i18n/fr/` mirrors the repository root, so `docs/i18n/fr/docs/architecture.md` +is the French `docs/architecture.md`. + ## Code signing policy Every release asset is signed by the Project Colony organisation in CI, never on diff --git a/docs/architecture.md b/docs/architecture.md index 6f3f94c..bb161dc 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -107,8 +107,8 @@ When a token has no value, it is dropped together with the separators around it, so a movie without a year becomes `Title`, not `Title ()`. Titles are cleaned before use: every character that is not a letter or a digit becomes a space (so `Grey's` becomes `Grey s`, and no character a file name cannot hold -survives), technical tags (`1080p`, `BluRay` and so on) are removed, and runs -of spaces collapse to one. +survives), technical tags (`1080p`, `BluRay`, `VF`, `VOSTFR` and so on) are +removed, and runs of spaces collapse to one. ## Rename engine diff --git a/docs/filebot-parity.md b/docs/filebot-parity.md index 0e59358..4001428 100644 --- a/docs/filebot-parity.md +++ b/docs/filebot-parity.md @@ -15,8 +15,10 @@ version exists. **Not yet** means there is no code for it. | Keep the extension | **Has it** | | Handle name collisions | **Has it.** Never overwrites; a taken name gets ` (1)`, ` (2)` and so on, and two files of a batch never collide | | Preview and dry run | **Has it.** Every new name is shown first, and a dry run renames nothing | -| Move files into series, season or movie folders | **Not yet.** Files are renamed in their own folder | +| Move files into series, season, movie or collection folders | **Not yet.** Files are renamed in their own folder | | Rename subtitles with their video | **Not yet** | +| Normalise subtitle language suffixes (`.fr`, `.en`, `.multi`) | **Not yet** | +| Download subtitles | **Not yet** | | Write `.nfo` files or file tags | **Not yet** | | Check SFV or other checksums | **Not yet** | diff --git a/docs/metadata-sources.md b/docs/metadata-sources.md index 053f4f6..8e251e1 100644 --- a/docs/metadata-sources.md +++ b/docs/metadata-sources.md @@ -50,6 +50,12 @@ TheTVDB, TVmaze and OMDb. searches. When the metadata language and region in Preferences are both unset, KayaBot sends `fr-FR`. A language alone gets its usual region (`en` gets `US`, `fr` gets `FR`) and a region alone its usual language. +- What KayaBot keeps: the localised title, with the original title as an alias + when it differs; the year from `release_date` (movies) or `first_air_date` + (series); and `vote_average` divided by 10 as the source rating. +- Quota: no fixed quota since December 2019, but TMDB still answers with a + `429` somewhere around 40 requests per second + ([rate limiting](https://developer.themoviedb.org/docs/rate-limiting)). - Terms: the [API terms of use](https://www.themoviedb.org/api-terms-of-use) require the notice "This product uses the TMDB API but is not endorsed or certified by TMDB" and follow the @@ -66,6 +72,8 @@ TheTVDB, TVmaze and OMDb. `/refresh_token` and keeps it in memory only. - Locale: the language as a `language` query parameter and, with the region, as `Accept-Language`. +- What KayaBot keeps: the series name, its `year`, and the `score` of the + search result as the source rating. - Terms: TheTVDB is free for projects earning less than $50k a year, against attribution with a direct link to TheTVDB.com, shown to the people who see its data ([API information](https://thetvdb.com/api-information)). The @@ -81,6 +89,10 @@ TheTVDB, TVmaze and OMDb. identifies them; KayaBot sends `KayaBot` unless `tvmaze_user_agent` or `KAYABOT_TVMAZE_USER_AGENT` sets another one. - Locale: none. +- What KayaBot keeps: the show name, the year from `premiered`, and the + `score` of the search result (the show's own score when that is 0) as the + source rating. +- Quota: at least 20 calls every 10 seconds per IP address. - Terms: the [API](https://www.tvmaze.com/api) is licensed under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/): TVmaze must be credited as the source, and shared data follows the ShareAlike terms. @@ -91,6 +103,11 @@ TheTVDB, TVmaze and OMDb. - Requests: `?s=` to search and `?i=<IMDb ID>` for a movie's details. - Authentication: the key goes in the `apikey` query parameter. - Locale: none. +- What KayaBot keeps: the title, the first four digits of `Year`, and the IMDb + ID, which is also the result's ID. OMDb gives no rating KayaBot uses: every + result gets a fixed source rating of 0.7. +- Quota: a free key allows 1,000 requests a day + ([API keys](https://www.omdbapi.com/apikey.aspx)). - Terms: OMDb content is licensed under [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/), and OMDb is not endorsed by or affiliated with IMDb.com From 0568681c0fdd4ff2316a432dcf3c9928d9cebb74 Mon Sep 17 00:00:00 2001 From: MotherSphere <linhajahad@gmail.com> Date: Thu, 15 Oct 2026 01:47:53 +0200 Subject: [PATCH 2/2] docs: bring back the French documentation as translations of the current pages --- .github/workflows/ci.yml | 20 ++ docs/i18n/fr/README.md | 289 ++++++++++++++++++++++++++ docs/i18n/fr/docs/architecture.md | 193 +++++++++++++++++ docs/i18n/fr/docs/filebot-parity.md | 65 ++++++ docs/i18n/fr/docs/metadata-sources.md | 137 ++++++++++++ scripts/i18n-check.sh | 132 ++++++++++++ scripts/i18n-links.py | 59 ++++++ 7 files changed, 895 insertions(+) create mode 100644 docs/i18n/fr/README.md create mode 100644 docs/i18n/fr/docs/architecture.md create mode 100644 docs/i18n/fr/docs/filebot-parity.md create mode 100644 docs/i18n/fr/docs/metadata-sources.md create mode 100755 scripts/i18n-check.sh create mode 100755 scripts/i18n-links.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4297c9b..f89e576 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,3 +79,23 @@ jobs: rustc --version | grep -q "^rustc ${RUSTUP_TOOLCHAIN}[. ]" \ || { echo "::error::expected rustc ${RUSTUP_TOOLCHAIN}, got $(rustc --version)"; exit 1; } cargo check --locked --all-targets + + i18n: + name: translations up to date + runs-on: ubuntu-latest + steps: + # No `fetch-depth` needed, and that is the point: the check compares the + # BLOB HASH of each English source against the stamp its translations + # carry, which is computable from the working tree alone. The obvious + # alternative, `git log` on the source, reports a single commit on the + # depth-1 clone Actions makes by default, so every comparison would come + # out equal and this job would pass green forever while translations rot. + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Check every translation against its English source + run: ./scripts/i18n-check.sh + + - name: Check every link and anchor resolves + run: ./scripts/i18n-links.py diff --git a/docs/i18n/fr/README.md b/docs/i18n/fr/README.md new file mode 100644 index 0000000..1376345 --- /dev/null +++ b/docs/i18n/fr/README.md @@ -0,0 +1,289 @@ +<!-- eidos-i18n: source=README.md sha=eda50a54468479b46b52f47c8ceb98dec5a37c22 --> + +<div align="center"> + +**Un renommeur de médias de bureau pour les bibliothèques de films et de séries : il cherche vos fichiers dans TMDB, TheTVDB, TVmaze et OMDb et leur donne des noms propres et cohérents.** + +</div> + +[![License: GPL-3.0-or-later](https://img.shields.io/badge/License-GPL--3.0--or--later-blue.svg)](../../../LICENSE) +[![Colony app](https://img.shields.io/badge/Colony-multimedia-purple)](https://github.com/Project-Colony/Colony) +[![Platforms](https://img.shields.io/badge/platforms-linux%20%7C%20windows%20%7C%20macOS-lightgrey)](#installation) + +Les épisodes et films téléchargés arrivent avec des noms comme +`show.name.s01e02.1080p.web.x264-grp.mkv`. Un renommeur de masse peut +nettoyer la ponctuation, mais il ne connaît pas le titre de l'épisode, et +FileBot, l'outil habituel pour ce travail, demande une licence payante. +KayaBot analyse les noms de fichiers, interroge plusieurs bases de données en +ligne à la fois et affiche chaque nouveau nom avant de toucher au moindre +fichier. + +> **État :** prototype précoce, et aucune version n'a encore été publiée. La +> vue Renommer fonctionne de bout en bout : analyse, correspondance avec TMDB, +> TheTVDB, TVmaze et OMDb, aperçu, simulation avec une confirmation avant un +> vrai renommage, le renommage lui-même et un résumé en CSV ou JSON. +> L'interface est construite avec iced et le système de design Colony, en +> anglais et en français ; les Préférences regroupent le thème, +> l'accessibilité, la langue, les clés d'API et les options de renommage. +> L'analyseur, les modèles de noms, le moteur de renommage, les quatre clients +> de sources et la gestion d'état de l'interface ont des tests automatisés, +> les clients face à des réponses enregistrées ; l'apparence de la fenêtre +> n'est vérifiée qu'à la main. + +## Pourquoi KayaBot + +- **Un renommeur de masse ou une regex** range la ponctuation, mais vous + tapez encore chaque titre d'épisode. KayaBot récupère les titres et l'année. +- **FileBot** demande une licence payante. KayaBot est un logiciel libre sous + GPL-3.0-or-later. +- **Aucune base de données n'est jamais complète.** KayaBot interroge + plusieurs sources en parallèle et fusionne leurs réponses par identifiants + communs (IMDb, TMDB, TheTVDB, TVmaze) et similarité des titres : une série + absente d'une source est quand même trouvée. +- **Le renommage est l'étape risquée.** KayaBot n'écrase jamais un fichier : + une simulation montre d'abord chaque résultat, et un nom déjà pris reçoit un + suffixe numéroté. + +``` +show.name.s01e02.1080p.web.x264-grp.mkv + | analyse : "show name", saison 1, épisode 2 + v +TheTVDB + TVmaze (séries) ou TMDB + OMDb (films), interrogés en parallèle + | meilleur titre, puis sa liste d'épisodes + v +Show Name 1x2 - Episode Title.mkv aperçu, simulation, renommage +``` + +## Ce qu'il fait + +- Analyse les noms de fichiers : `S01E02`, `S01 E02`, `1x02`, `102`, + `Season 1 Episode 2` (ou `Saison`), une année de sortie, et retire les tags + de qualité, de codec, d'audio et de groupe de diffusion. +- Devine si chaque fichier est un épisode ou un film. Vous pouvez changer le + type. +- Cherche les séries dans TheTVDB et TVmaze (TMDB sur demande) et les films + dans TMDB et OMDb. La source choisie et son partenaire sont interrogés en + parallèle ; l'option « Utiliser cette source uniquement » limite une + recherche à la source choisie. Les recherches tournent en arrière-plan : la + fenêtre reste réactive. +- Classe les résultats fusionnés selon la confiance accordée à la source, la + similarité des titres et la note propre à la source, puis choisit l'épisode + par saison et numéro. +- Construit les noms à partir de modèles. Par défaut, `{n} {s}x{e} - {t}` pour + les épisodes et `{n} ({y})` pour les films ; des modèles prédéfinis et + personnalisés sont disponibles, et une valeur manquante disparaît avec son + séparateur. +- Affiche chaque nouveau nom, propose une simulation, demande confirmation + avant un vrai renommage (vous pouvez le désactiver) et renomme sur place. Il + conserve l'extension, n'écrase jamais un fichier (un nom pris reçoit ` (1)`, + ` (2)` et ainsi de suite) et gère les renommages qui ne changent que la + casse. +- Exporte le résultat d'un passage en CSV ou en JSON. + +## Installation + +### Via Colony (recommandé) + +Cherchez **KayaBot** dans [Colony](https://github.com/Project-Colony/Colony) et +installez-le. Les mises à jour arrivent par le lanceur. KayaBot y apparaîtra +dès la publication de sa première version. + +### Téléchargement direct du binaire + +Aucune version n'a encore été publiée. Chaque version portera ces fichiers sur +la page de la [dernière version](https://github.com/Project-Colony/KayaBot/releases/latest) : + +| Plateforme | Fichier | +|---|---| +| Linux | `kayabot-linux` | +| Windows | `kayabot-windows.exe` | +| macOS (Apple Silicon) | `kayabot-macos` | +| macOS (Intel) | `kayabot-macos-x86` | + +```bash +chmod +x kayabot-linux && ./kayabot-linux +``` + +### Compiler depuis les sources + +```bash +git clone https://github.com/Project-Colony/KayaBot +cd KayaBot +cargo build --release +./target/release/kayabot +``` + +Nécessite Rust 1.88 ou plus récent. `kayabot --version` affiche la version et +quitte sans ouvrir de fenêtre. + +## Configuration + +KayaBot garde ses fichiers dans un seul dossier : + +| Plateforme | Dossier | +|---|---| +| Linux | `~/.config/Colony/KayaBot/` | +| Windows | `%LOCALAPPDATA%\Colony\KayaBot\` | +| macOS | `~/Library/Application Support/Colony/KayaBot/` | + +| Fichier | Contenu | +|---|---| +| `preferences.json` | les choix faits dans les Préférences, enregistrés au fur et à mesure | +| `api_keys.toml` | vos clés d'API, saisies dans les Préférences, rubrique Connexions, ou écrites à la main | +| `format_options.toml` | les modèles de noms, et si les noms portent l'année et le titre de l'épisode | + +Les anciennes versions gardaient ces fichiers dans `~/.kayabot/` et, sous +Windows, dans le profil itinérant, `%APPDATA%\Colony\KayaBot\`. Tant que le +dossier ci-dessus est vide, le premier démarrage les y copie et laisse un +marqueur `.migrated`. L'ancien dossier reste tel quel, à supprimer à la main ; +si la copie échoue, KayaBot continue d'utiliser l'ancien dossier et réessaie +au démarrage suivant. Un `preferences.toml` d'une ancienne version est lu une +fois et enregistré en `preferences.json`. + +TVmaze n'a pas besoin de clé. TMDB accepte soit un jeton d'accès en lecture +v4, soit une clé API v3, et TheTVDB et OMDb ont chacun besoin de leur propre +clé : + +```toml +tmdb_bearer_token = "..." # ou : tmdb_api_key = "..." +tvdb_api_key = "..." +omdb_api_key = "..." +tvmaze_user_agent = "KayaBot" +``` + +Les variables d'environnement ont priorité sur le fichier. Elles sont listées +dans [`.env.example`](../../../.env.example) : `KAYABOT_TMDB_BEARER_TOKEN` (ou +`KAYABOT_TMDB_API_KEY`), `KAYABOT_TVDB_API_KEY`, `KAYABOT_OMDB_API_KEY` et +`KAYABOT_TVMAZE_USER_AGENT`. `KAYABOT_TVMAZE_API_KEY` est un ancien nom de +l'agent utilisateur TVmaze, lu seulement quand `KAYABOT_TVMAZE_USER_AGENT` +n'est pas défini ; TVmaze n'a pas de clé d'API. + +Les anciennes versions lisaient aussi les clés dans `config.toml`, dans le +même dossier. Au démarrage, KayaBot déplace chaque clé qu'il y trouve vers +`api_keys.toml`, sauf si ce fichier la définit déjà, et retire toutes les clés +de `config.toml`. + +## Documentation + +- [docs/architecture.md](docs/architecture.md) : l'organisation du code et le + chemin d'un fichier de son ancien nom à son nouveau nom. +- [docs/metadata-sources.md](docs/metadata-sources.md) : ce que couvre chaque + source de métadonnées, comment KayaBot s'authentifie et ce qu'il envoie. +- [docs/filebot-parity.md](docs/filebot-parity.md) : les fonctionnalités de + FileBot que KayaBot a aujourd'hui et celles qu'il n'a pas. + +## Langue + +Ce README et les pages ci-dessus existent aussi en français. **L'anglais fait +foi** : quand une traduction le contredit, c'est le fichier anglais qui a +raison. + +- **Français** - [README](README.md) · [architecture](docs/architecture.md) · [sources de métadonnées](docs/metadata-sources.md) · [parité avec FileBot](docs/filebot-parity.md) + +Chaque traduction porte l'empreinte du fichier anglais dont elle est issue, et +la CI échoue quand l'anglais avance : voir [`scripts/i18n-check.sh`](../../../scripts/i18n-check.sh). +`docs/i18n/fr/` reproduit la racine du dépôt : `docs/i18n/fr/docs/architecture.md` +est la version française de `docs/architecture.md`. + +## Politique de signature du code + +Chaque fichier de version est signé par l'organisation Project Colony dans la +CI, jamais sur la machine d'un développeur. À côté de chaque fichier, sur la +page de la version : + +| Fichier | Ce que c'est | +|---|---| +| `<asset>.sig` | une signature ed25519 du fichier, faite avec la clé de publication de l'organisation | +| `<asset>.meta` | trois lignes qui lient le fichier à son nom, à son sha256 et au numéro de version | +| `<asset>.meta.sig` | une signature ed25519 du `.meta` | + +La clé privée est un secret de l'organisation, utilisé uniquement par le +[workflow sign-and-publish](https://github.com/Project-Colony/Project-Colony-Resources/blob/main/.github/workflows/sign-and-publish.yml) +partagé, dans un job qui ne compile rien ; les jobs qui compilent KayaBot ne +la voient jamais. Colony vérifie les trois fichiers avant d'installer ou de +mettre à jour KayaBot, et refuse une version plus ancienne que celle qui est +installée. Pour vérifier vous-même un téléchargement avec OpenSSL 3 (les mêmes +commandes valent pour chaque fichier) : + +```bash +cat > colony-release.pub <<'EOF' +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEARNjg3Nn8H6/aBg1unwGjkUTcrdTxERNefVaqU8cFu0s= +-----END PUBLIC KEY----- +EOF +a=kayabot-linux +openssl pkeyutl -verify -pubin -inkey colony-release.pub -rawin -in "$a" -sigfile "$a.sig" +openssl pkeyutl -verify -pubin -inkey colony-release.pub -rawin -in "$a.meta" -sigfile "$a.meta.sig" +cat "$a.meta" # version=<tag>, asset=<nom du fichier>, sha256=<condensé> +sha256sum "$a" # le condensé doit être égal à la ligne sha256 +``` + +Comment les versions sont compilées, signées et publiées : +[design/releases.md](https://github.com/Project-Colony/Project-Colony-Resources/blob/main/design/releases.md#5-signing) +(en anglais). + +<!-- +N'ajouter ce qui suit qu'une fois que SignPath signe la version Windows, +c'est-à-dire une fois que `signpath-project-slug` est défini dans +.github/workflows/release.yml. Avant cela, ce texte promettrait une signature +que le .exe ne porte pas. + +Les versions Windows sont aussi signées Authenticode. Signature de code +gratuite fournie par [SignPath.io](https://about.signpath.io/), certificat +fourni par la [SignPath Foundation](https://signpath.org/). + +- Contributeurs et relecteurs : <comptes GitHub autorisés à modifier les sources> +- Approbateurs : <comptes GitHub qui approuvent chaque demande de signature dans SignPath> +--> + +## Confidentialité + +KayaBot n'envoie ni télémétrie, ni statistiques d'usage, ni rapports de +plantage, et il n'a ni compte ni vérification de mises à jour. + +| Donnée | Conservée ou envoyée | Où, et pourquoi | +|---|---|---| +| Préférences | conservées | `preferences.json` et `format_options.toml` dans le dossier indiqué sous [Configuration](#configuration) | +| Fichiers des anciennes versions | copiés | une fois, depuis `~/.kayabot/` ou le profil itinérant de Windows vers le dossier indiqué sous [Configuration](#configuration). Rien ne quitte l'ordinateur, et les anciennes copies, clés d'API comprises, restent où elles étaient jusqu'à ce que vous les supprimiez | +| Clés d'API | conservées | `api_keys.toml` dans le même dossier, en clair. Sous Linux et macOS, KayaBot l'écrit lisible par votre seul utilisateur (mode 0600) et resserre à ce mode un fichier existant quand il le lit. Les clés fournies par variables d'environnement ne sont jamais écrites sur le disque | +| Clés laissées dans `config.toml` par les anciennes versions | déplacées | vers `api_keys.toml` au démarrage, puis retirées de `config.toml` | +| Titres à rechercher | envoyés | quand vous lancez une association : le titre tiré du nom de fichier, ou celui que vous avez saisi, plus l'année pour un film, aux sources actives ci-dessous. Le nom du fichier, son dossier et son contenu ne sont jamais envoyés, pas plus que les numéros de saison ou d'épisode : KayaBot télécharge la liste des épisodes de la série et choisit l'épisode localement | +| Identifiants de titres | envoyés | l'identifiant de la série ou du film choisi, à la source qui l'a renvoyé, pour récupérer sa liste d'épisodes ou ses détails | +| Langue et région des métadonnées | envoyées | à TMDB (en `language` et `region`, `fr-FR` quand les Préférences laissent les deux vides) et à TheTVDB | +| Clé TMDB | envoyée | à `api.themoviedb.org` uniquement : un jeton v4 dans l'en-tête `Authorization`, une clé v3 en paramètre `api_key` dans l'URL de la requête | +| Clé TheTVDB | envoyée | à `api4.thetvdb.com` uniquement, dans le corps de sa requête de connexion ; le jeton de session qu'il renvoie est gardé en mémoire et jamais enregistré | +| Clé OMDb | envoyée | à `www.omdbapi.com` uniquement, en paramètre `apikey` dans l'URL de la requête | +| Agent utilisateur | envoyé | à `api.tvmaze.com`, `KayaBot` sauf si vous définissez `tvmaze_user_agent`. TVmaze n'a pas besoin de clé : il est donc interrogé pour les séries sans aucune configuration | +| Résumé du renommage | conservé | seulement quand vous l'exportez, en CSV ou JSON, au chemin que vous choisissez | +| Vos fichiers médias | renommés | sur place, dans leur propre dossier. Rien n'est téléversé | + +KayaBot ne garde aucun cache de métadonnées : chaque association interroge de +nouveau les sources. Il ne se connecte à aucun serveur en dehors des quatre +ci-dessus, et seulement quand vous lancez une association. Une source sans clé +est ignorée sans aucune requête. Les clés envoyées dans une URL (la clé OMDb +et une clé TMDB v3) peuvent apparaître dans les journaux d'un proxy situé +entre vous et le service ; un jeton TMDB v4 évite cela. Comme toute requête +HTTPS, chacune montre aussi votre adresse IP au service. + +### Services tiers + +Chaque service traite ce qu'il reçoit selon sa propre politique : + +- **TMDB** : [politique de confidentialité](https://www.themoviedb.org/privacy-policy). + Ce produit utilise l'API TMDB mais n'est ni approuvé ni certifié par + [TMDB](https://www.themoviedb.org/) + ([règles de logo et d'attribution](https://www.themoviedb.org/about/logos-attribution)). +- **TheTVDB** : [politique de confidentialité](https://thetvdb.com/privacy-policy). + Métadonnées fournies par [TheTVDB](https://thetvdb.com/). Pensez à y ajouter + les informations manquantes ou à vous abonner. +- **TVmaze** : [politique de confidentialité](https://www.tvmaze.com/site/privacy). + Données TV de [TVmaze](https://www.tvmaze.com/), sous licence + [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). +- **OMDb** : [conditions](https://www.omdbapi.com/legal.htm). Données de films + de l'[API OMDb](https://www.omdbapi.com/), sous licence + [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/). OMDb n'est + ni approuvé par IMDb.com ni affilié à IMDb.com. + +## Licence + +GPL-3.0-or-later. Voir [LICENSE](../../../LICENSE). diff --git a/docs/i18n/fr/docs/architecture.md b/docs/i18n/fr/docs/architecture.md new file mode 100644 index 0000000..b15d1a8 --- /dev/null +++ b/docs/i18n/fr/docs/architecture.md @@ -0,0 +1,193 @@ +<!-- eidos-i18n: source=docs/architecture.md sha=bb161dc30570220a4ac991fe4c8dbbc08e2b75f4 --> + +# Architecture + +KayaBot est une seule crate Rust, avec une bibliothèque (`kayabot`, +`src/lib.rs`) et un programme de bureau (`src/main.rs`). La bibliothèque +contient tout, interface comprise ; `main.rs` se contente de répondre à +`--version`, de choisir le dossier de configuration et d'ouvrir la fenêtre. + +## Organisation du code + +| Chemin | Rôle | +|---|---| +| `src/main.rs` | le point d'entrée : `--version`, le dossier de configuration, puis `ui::run` | +| `src/ui/mod.rs` | `run` : applique les préférences, enregistre les polices et démarre iced | +| `src/ui/app.rs` | `App`, chaque `Message` qu'il traite, le cadre autour des vues, le dépôt de fichiers et les raccourcis clavier | +| `src/ui/sidebar.rs` | la barre latérale : le bouton KayaBot qui ouvre les Préférences, puis l'entrée Renommer | +| `src/ui/preferences.rs` | la page Préférences : Général, Apparence et Accessibilité venant de colony-ui, puis Connexions, Renommage et À propos | +| `src/ui/rename/mod.rs` | l'état et les messages de la vue Renommer, le renommage lui-même et l'aperçu des noms | +| `src/ui/rename/files.rs`, `preview.rs`, `details.rs`, `summary.rs` | la liste des fichiers, les noms proposés avec les options de recherche et de nommage, la fenêtre de correspondance, et le résumé avec son export et la fenêtre de confirmation | +| `src/ui/widgets.rs` | les boutons, cartes, champs et fenêtres propres à KayaBot, stylés d'après la palette, et ses glyphes Nerd Font | +| `src/i18n/` | `t` et `tf`, avec `en.json` et `fr.json` ; le squelette des Préférences et les libellés des thèmes viennent de `colony_ui::i18n` | +| `src/config.rs` | `Config`, enregistré dans `preferences.json`, et la conversion de l'ancien `preferences.toml` | +| `src/api_keys.rs` | les clés venant de l'environnement et de `api_keys.toml`, `write_private_file` et `migrate_config_credentials` | +| `src/lookup.rs` | quelles sources une recherche interroge, ce qu'elle leur demande, et les recherches que l'interface lance hors de son fil d'exécution | +| `src/export.rs` | le résumé du renommage en CSV ou JSON | +| `src/paths.rs` | le dossier de configuration, venant de `colony_ui::paths`, et la copie unique depuis les anciens emplacements | +| `src/matching/parsing.rs` | `parse_filename` : découpe un nom de fichier en jetons et en tire un titre, une saison et un épisode, et une année | +| `src/matching/mod.rs` | `match_files` : transforme chaque nom analysé en un `MatchResult` avec ses candidats (épisode, film ou série), un statut et une confiance | +| `src/matching/advanced.rs` | `normalize_name` et `rank_candidates`, un score Jaro-Winkler et Levenshtein. La chaîne de traitement s'en sert pour la similarité de titre de chaque résultat | +| `src/metadata/provider.rs` | le trait `MetadataProvider` (`search_title`, `fetch_episode_list`, `fetch_movie_details`) et l'énumération `MetadataSource` | +| `src/metadata/providers/` | un client par source : `tmdb.rs`, `thetvdb.rs`, `tvmaze.rs`, `omdb.rs` | +| `src/metadata/aggregate.rs` | `MetadataPipeline` : interroge les sources actives en parallèle, fusionne et classe leurs réponses | +| `src/metadata/models.rs` | `TitleMatch`, `EpisodeMatch`, `MovieMatch` et les `ExternalIds` et informations complémentaires qu'ils portent | +| `src/metadata/locale.rs` | `MetadataLocale`, la langue et la région envoyées aux sources | +| `src/metadata/error.rs` | `MetadataError`, les erreurs qu'un client signale à la fenêtre | +| `src/formatting.rs` | les modèles de noms, le nettoyage des titres et `format_options.toml` | +| `src/rename.rs` | le moteur de renommage | + +## L'interface + +La fenêtre est construite avec [iced](https://iced.rs) 0.14 et +[colony-ui](https://crates.io/crates/colony-ui), le système de design que +partagent tous les programmes Colony. Les couleurs viennent de la palette +Colony active, le texte des trois polices Colony via `Typography::current()` +(ainsi la police pour la dyslexie et les deux tailles de texte atteignent +chaque widget), et les tailles passent par `sz()`. Les widgets intégrés d'iced +reçoivent la palette via `App::theme`. + +`App` suit le modèle d'iced : `update` reçoit un `Message` et modifie l'état, +`view` le dessine. La vue Renommer est un composant à part entière +(`ui::rename::Rename`) avec son propre `Message`, enveloppé dans +`Message::Rename`. Les Préférences remplacent la zone de contenu, jamais une +fenêtre ni une boîte de dialogue, et le bouton KayaBot comme le bouton Fermer +de la page envoient le même `Message::TogglePreferences`. Une modification +dans les Préférences s'applique tout de suite et est enregistrée ; il n'y a +pas de bouton Enregistrer. + +## D'un ancien nom à un nouveau + +1. **Ajouter des fichiers.** La vue Renommer accepte des fichiers ou un + dossier choisis dans la boîte de dialogue du système (`rfd`), ou des + fichiers et dossiers déposés sur la fenêtre. +2. **Analyser.** `matching::match_files` lit chaque nom. `S01E02`, `S01 E02`, + `1x02`, un nombre à trois chiffres `102` et `Season 1 Episode 2` (ou + `Saison`) donnent une saison et un épisode ; un nombre à quatre chiffres de + 1900 à 2099 donne une année. Les tags de qualité, de codec, d'audio et de + groupe de diffusion sont retirés du titre. Une saison et un épisode font du + fichier un épisode ; une année sans eux en fait un film. Vous pouvez changer + le type dans la vue. +3. **Choisir les sources.** Par défaut, les séries utilisent TheTVDB avec + TVmaze pour partenaire, et les films TMDB avec OMDb. La liste active est la + source choisie plus ses partenaires (TMDB choisi pour une série amène à la + fois TheTVDB et TVmaze). La case « Utiliser cette source uniquement » ne + garde que la source choisie. +4. **Chercher.** `MetadataPipeline::search_title` envoie le titre (plus + l'année pour un film) à toutes les sources actives en même temps, sur des + threads à portée limitée (scoped threads), et attend qu'elles aient toutes + répondu. Une source qui échoue ou n'a pas de clé est écartée ; la recherche + n'échoue que si toutes les sources échouent. +5. **Fusionner et classer.** Les résultats qui partagent un identifiant externe + (IMDb, TMDB, TheTVDB, TVmaze, OMDb), ou dont les titres sont similaires à au + moins 0.9 avec des années compatibles, ne forment qu'une entrée. Chaque + entrée reçoit le score + `0.55 * trust + 0.35 * title similarity + 0.10 * source rating` (confiance, + similarité du titre, note de la source), où la confiance vaut 0.9 pour TMDB + et TheTVDB, 0.75 pour TVmaze et 0.7 pour OMDb. +6. **Épisode.** Pour un épisode, la liste des épisodes du meilleur titre est + récupérée auprès de la source qui l'a renvoyé, et l'entrée qui porte la + saison et le numéro analysés est retenue. +7. **Mettre en forme.** `formatting` applique le modèle à chaque fichier (voir + plus bas). +8. **Aperçu, simulation, renommage.** `rename::apply` construit la cible à + côté de l'original et conserve l'extension. En mode simulation, il résout le + nom et ne touche à rien ; sinon, une fois que l'utilisateur a confirmé (sauf + si c'est désactivé dans les Préférences), il renomme, puis la liste se + recharge à partir des nouveaux noms. Le résultat de chaque fichier alimente + le résumé, qui peut être exporté. + +Les clients des sources utilisent du HTTP bloquant avec un délai d'expiration +de 10 secondes. L'interface ne les appelle jamais elle-même : `Rename::run` +confie la recherche à un thread de travail et en reçoit la réponse sous forme +de message, et Associer et Rechercher restent désactivés pendant ce temps. Une +association lancée avant que la liste de fichiers ne change est abandonnée à +son arrivée. + +## Modèles de noms + +| Jeton | Valeur | +|---|---| +| `{n}` | le titre de la série ou du film (aussi `{name}`, `{title}`) | +| `{t}` | le titre de l'épisode (aussi `{episodetitle}`) | +| `{s}` | le numéro de saison (aussi `{season}`) | +| `{e}` | le numéro d'épisode (aussi `{episode}`) | +| `{y}` | l'année (aussi `{year}`) | + +Des chiffres après un jeton complètent le nombre par des zéros : `{s00}` donne +`01`. Les modèles par défaut sont `{n} {s}x{e} - {t}` pour les épisodes et +`{n} ({y})` pour les films ; désactiver le titre de l'épisode ou l'année dans +la vue les fait passer à `{n} {s}x{e}` et `{n}`. Quand un jeton n'a pas de +valeur, il disparaît avec les séparateurs qui l'entourent : un film sans année +devient `Title`, pas `Title ()`. Les titres sont nettoyés avant usage : tout +caractère qui n'est ni une lettre ni un chiffre devient une espace (ainsi +`Grey's` devient `Grey s`, et aucun caractère interdit dans un nom de fichier +ne subsiste), les tags techniques (`1080p`, `BluRay`, `VF`, `VOSTFR`, etc.) +sont supprimés, et les espaces multiples sont réduites à une seule. + +## Moteur de renommage + +`src/rename.rs` construit le chemin cible à côté de l'original, choisit le +premier nom libre (` (1)`, ` (2)` et ainsi de suite, jusqu'à 9999) et renomme +le fichier. Il n'écrase jamais un fichier existant, y compris un lien physique +vers l'original. Deux fichiers d'un même lot ne reçoivent jamais des noms qui +ne diffèrent que par la casse. Un renommage qui ne change que la casse passe +par un nom temporaire ; un déplacement entre deux périphériques copie dans un +nouveau fichier, le synchronise sur le disque, puis supprime l'original. + +## Fichiers de configuration + +`paths::config_dir` vaut `colony_ui::paths::config_dir("KayaBot")` : +`~/.config/Colony/KayaBot` sous Linux, `%LOCALAPPDATA%\Colony\KayaBot` sous +Windows et `~/Library/Application Support/Colony/KayaBot` sous macOS. + +Avant toute lecture, `paths::init` vérifie ce dossier. S'il est vide et qu'un +ancien emplacement contient des fichiers (d'abord le profil itinérant de +Windows, `%APPDATA%\Colony\KayaBot`, puis `~/.kayabot`), il les copie et écrit +un marqueur `.migrated`. L'ancien dossier n'est jamais modifié. Une copie qui +échoue est annulée, et l'ancien dossier est utilisé pour cette exécution. + +- `preferences.json` : `Config`, c'est-à-dire les `StandardPreferences` de + colony-ui (thème, couleur d'accent, tailles de texte, contraste élevé, + police pour la dyslexie, animations réduites), plus la langue de + l'interface, la langue et la région des métadonnées, les deux confirmations + et le mode simulation par défaut. Chaque champ est lu séparément : une valeur + invalide ne réinitialise que ce champ. Écrit à chaque changement de + préférence. S'il manque, le `preferences.toml` d'une ancienne version, à plat + ou en sections, est lu une fois et enregistré au nouveau format ; ses + réglages qui n'ont jamais rien fait sont abandonnés. +- `api_keys.toml` : lu au démarrage ; écrit au fil de la saisie des clés dans + Préférences, Connexions, via `write_private_file`, qui écrit un fichier + temporaire avec le mode 0600 sous Unix et le renomme à sa place. Les + variables d'environnement priment sur chaque clé. +- `format_options.toml` : les modèles de noms et les choix concernant l'année + et le titre de l'épisode. +- `config.toml` : seulement pour les anciennes versions. Avant l'ouverture de + la fenêtre, `migrate_config_credentials` déplace ses clés dans + `api_keys.toml` et les en retire, identifiants AniDB compris. + +## Tests + +- Les tests unitaires se trouvent à côté du code : les modèles, le moteur de + renommage, la chaîne de traitement, chaque client de source face aux + réponses enregistrées dans `tests/fixtures/`, la migration des clés et les + écritures de fichiers privés, l'export, les préférences et leur conversion, + la copie depuis les anciens dossiers, et la fonction `update` de l'interface + avec une source simulée (`ui/app.rs`, `ui/rename/tests.rs`). Les tests + unitaires écrivent dans un dossier temporaire, jamais dans le vrai profil. +- `i18n` vérifie que `en.json` et `fr.json` ont les mêmes clés et les mêmes + paramètres, et que chaque clé demandée par l'interface existe ; `ui/widgets.rs` + vérifie que chaque glyphe existe dans la Nerd Font. +- `tests/matching_snapshots.rs` passe l'analyseur sur un épisode, un film et + un titre seul et compare le résultat à un instantané `insta`. Les fichiers + vides de `tests/fixtures/media_samples` alimentent les tests du renommage et + de la vue Renommer. +- `tests/integration_api.rs` appelle les vrais services. Ses tests sont + ignorés par défaut et ont besoin des clés de `.env.example` : + + ```bash + KAYABOT_TMDB_API_KEY=... KAYABOT_OMDB_API_KEY=... KAYABOT_TVDB_API_KEY=... \ + cargo test --test integration_api -- --ignored + ``` + + `KAYABOT_TMDB_BEARER_TOKEN` fonctionne à la place de `KAYABOT_TMDB_API_KEY`. diff --git a/docs/i18n/fr/docs/filebot-parity.md b/docs/i18n/fr/docs/filebot-parity.md new file mode 100644 index 0000000..5b5be29 --- /dev/null +++ b/docs/i18n/fr/docs/filebot-parity.md @@ -0,0 +1,65 @@ +<!-- eidos-i18n: source=docs/filebot-parity.md sha=40014280219be7504849fc6b64aef7592b0a9b2f --> + +# Parité avec FileBot + +FileBot est la référence de ce qu'un renommeur de médias doit savoir faire. +Cette page liste les fonctionnalités de FileBot que KayaBot vise à couvrir et +où chacune en est aujourd'hui, pour que personne n'ait à lire le code pour le +savoir. + +**Oui** signifie que cela fonctionne dans le programme actuel. **En partie** +signifie qu'une version réduite existe. **Pas encore** signifie qu'aucun code +n'existe pour cela. + +## Renommage et organisation + +| Fonctionnalité | KayaBot aujourd'hui | +|---|---| +| Renommer selon un modèle | **Oui.** Modèles prédéfinis et personnalisés pour les épisodes et les films | +| Conserver l'extension | **Oui** | +| Gérer les collisions de noms | **Oui.** N'écrase jamais rien ; un nom déjà pris reçoit ` (1)`, ` (2)` et ainsi de suite, et deux fichiers d'un même lot n'entrent jamais en collision | +| Prévisualisation et simulation | **Oui.** Chaque nouveau nom est affiché d'abord, et une simulation (dry run) ne renomme rien | +| Déplacer les fichiers dans des dossiers par série, saison, film ou collection | **Pas encore.** Les fichiers sont renommés dans leur propre dossier | +| Renommer les sous-titres avec leur vidéo | **Pas encore** | +| Normaliser les suffixes de langue des sous-titres (`.fr`, `.en`, `.multi`) | **Pas encore** | +| Télécharger des sous-titres | **Pas encore** | +| Écrire des fichiers `.nfo` ou des tags dans les fichiers | **Pas encore** | +| Vérifier les SFV ou d'autres sommes de contrôle | **Pas encore** | + +## Correspondance + +| Fonctionnalité | KayaBot aujourd'hui | +|---|---| +| Lire la saison, l'épisode et l'année dans un nom de fichier | **Oui.** `S01E02`, `S01 E02`, `1x02`, `102`, `Season 1 Episode 2`, une année de 1900 à 2099 | +| Distinguer un épisode d'un film | **Oui**, d'après le nom analysé ; vous pouvez changer le type | +| Identifier les séries et les épisodes | **Oui.** Recherche par titre, puis l'épisode par saison et numéro | +| Identifier les films | **Oui.** Titre et année | +| Choisir entre des résultats ambigus | **En partie.** Les candidats sont listés avec leur score et vous pouvez en choisir un autre, ou corriger à la main le titre, la saison et l'épisode d'un fichier. Il n'y a pas de question interactive pendant un lot | +| Identifier par empreinte (hash) du fichier | **Pas encore** | +| Éditions et versions (cuts), fichiers multi-épisodes, épisodes spéciaux | **Pas encore** | +| Filtrer les fichiers par règles | **Pas encore** | + +## Métadonnées + +| Fonctionnalité | KayaBot aujourd'hui | +|---|---| +| Plusieurs sources avec une priorité par type de contenu | **Oui.** TheTVDB et TVmaze pour les séries, TMDB et OMDb pour les films ; voir [metadata-sources.md](metadata-sources.md) | +| Fusionner les résultats de plusieurs sources | **Oui.** Par identifiants communs et similarité des titres | +| Se rabattre sur une autre source quand l'une échoue | **Oui.** Les partenaires sont interrogés en même temps et une source en échec est écartée | +| Titre officiel, année et titre d'épisode | **Oui** | +| Synopsis et genres | **En partie.** Affichés à côté d'une correspondance, pas utilisés dans les noms | +| Langue et région des titres | **Oui** pour TMDB et TheTVDB | +| Distribution, illustrations, collections | **Pas encore** | + +## Modèles + +KayaBot lit des jetons au format FileBot, un petit sous-ensemble du langage de +format de FileBot : + +| Jeton | KayaBot aujourd'hui | +|---|---| +| `{n}` titre, `{t}` titre d'épisode, `{y}` année | **Oui** | +| `{s}`, `{e}` et leurs formes complétées par des zéros comme `{s00}`, `{e00}` | **Oui** | +| Supprimer une valeur manquante avec son séparateur | **Oui** | +| `{airdate}` et les autres variables de FileBot | **Pas encore** | +| Expressions et conditions dans un modèle | **Pas encore** | diff --git a/docs/i18n/fr/docs/metadata-sources.md b/docs/i18n/fr/docs/metadata-sources.md new file mode 100644 index 0000000..e1cdf3c --- /dev/null +++ b/docs/i18n/fr/docs/metadata-sources.md @@ -0,0 +1,137 @@ +<!-- eidos-i18n: source=docs/metadata-sources.md sha=8e251e1a15d6dffea5f3e2cc80a6d4a8eb0a93f1 --> + +# Sources de métadonnées + +KayaBot cherche les titres dans quatre bases de données en ligne. Cette page +indique à quoi sert chacune, comment KayaBot l'interroge et ce que le service +demande à un programme qui l'utilise. Ce qui quitte votre machine est résumé +dans la section [Confidentialité](../README.md#confidentialité) du README. + +## En un coup d'œil + +| Source | Utilisée pour | Clé | Confiance | +|---|---|---|---| +| TMDB | les films par défaut ; les séries si vous la choisissez | un jeton d'accès en lecture v4 ou une clé API v3 | 0.9 | +| TheTVDB | les séries par défaut | une clé API | 0.9 | +| TVmaze | les séries, en partenaire de TheTVDB | aucune | 0.75 | +| OMDb | les films, en partenaire de TMDB | une clé API | 0.7 | + +La confiance est le poids fixe que chaque client donne à sa source quand les +résultats sont classés (voir +[architecture.md](architecture.md#dun-ancien-nom-à-un-nouveau)). Les libellés +affichés dans le programme proviennent de `MetadataSource::label` : TheMovieDB, +TheTVDB, TVmaze et OMDb. + +## Comment KayaBot choisit les sources + +- La liste Source de la carte Recherche, dans la vue Renommer, propose + TheMovieDB, TheTVDB et TVmaze quand le type est Série, et TheMovieDB et OMDb + quand c'est Film. +- Au démarrage, les séries utilisent TheTVDB et les films TheMovieDB. Votre + dernier choix pour chaque type est conservé jusqu'à la fermeture du + programme. +- Une recherche interroge ensemble la source choisie et ses partenaires : + TheTVDB avec TVmaze, TVmaze avec TheTVDB, TMDB (pour les séries) avec les + deux, et TMDB avec OMDb ou OMDb avec TMDB pour les films. Leurs réponses sont + fusionnées : les partenaires ne sont donc pas un repli essayé après un échec, + mais un second avis demandé en même temps. +- L'option « Utiliser cette source uniquement » n'interroge que la source + choisie. +- Une source sans clé renvoie une erreur avant toute requête et est écartée de + la fusion. TVmaze n'a pas besoin de clé : une recherche de série l'atteint + donc toujours. + +## Chaque source + +### TMDB + +- Hôte : `https://api.themoviedb.org/3`. +- Requêtes : `/search/multi` avec `query` et `include_adult=false` ; + `/tv/{id}` puis `/tv/{id}/season/{n}` pour chaque saison afin de construire + la liste des épisodes ; `/movie/{id}` pour un film. +- Authentification : une valeur qui commence par `eyJ` est un jeton d'accès en + lecture v4 et part dans l'en-tête `Authorization: Bearer` ; toute autre + valeur est une clé v3 et part dans le paramètre de requête `api_key`. +- Langue et région : `language` (par exemple `en-US`) sur chaque requête et + `region` sur les recherches. Quand la langue et la région des métadonnées + sont toutes deux vides dans les Préférences, KayaBot envoie `fr-FR`. Une + langue seule reçoit sa région habituelle (`en` reçoit `US`, `fr` reçoit `FR`) + et une région seule sa langue habituelle. +- Ce que KayaBot conserve : le titre localisé, avec le titre original comme + alias quand il diffère ; l'année tirée de `release_date` (films) ou de + `first_air_date` (séries) ; et `vote_average` divisé par 10 comme note de la + source. +- Quota : plus de quota fixe depuis décembre 2019, mais TMDB répond encore par + un `429` aux alentours de 40 requêtes par seconde + ([limitation de débit](https://developer.themoviedb.org/docs/rate-limiting)). +- Conditions : les [conditions d'utilisation de l'API](https://www.themoviedb.org/api-terms-of-use) + exigent la mention « This product uses the TMDB API but is not endorsed or + certified by TMDB » et le respect des + [règles de logo et d'attribution](https://www.themoviedb.org/about/logos-attribution). + +### TheTVDB + +- Hôte : `https://api4.thetvdb.com/v4`. +- Requêtes : `/search` avec `query` et `type=series` ; + `/series/{id}/episodes/default` pour une liste d'épisodes ; `/movies/{id}` + pour un film. +- Authentification : la clé est envoyée à `/login`, qui renvoie un jeton + transmis en `Authorization: Bearer` sur les autres requêtes. KayaBot le + renouvelle via `/refresh_token` et le garde en mémoire uniquement. +- Langue et région : la langue en paramètre de requête `language` et, avec la + région, en `Accept-Language`. +- Ce que KayaBot conserve : le nom de la série, son `year`, et le `score` du + résultat de recherche comme note de la source. +- Conditions : TheTVDB est gratuit pour les projets qui gagnent moins de + 50 000 $ par an, en échange d'une attribution avec un lien direct vers + TheTVDB.com, visible par les personnes qui voient ses données + ([informations sur l'API](https://thetvdb.com/api-information)). La + formulation suggérée est « Metadata provided by TheTVDB. Please consider + adding missing information or subscribing. » + +### TVmaze + +- Hôte : `https://api.tvmaze.com`. +- Requêtes : `/search/shows` avec `q` ; `/shows/{id}/episodes` pour une liste + d'épisodes. +- Authentification : aucune. TVmaze demande aux clients d'envoyer un + User-Agent qui les identifie ; KayaBot envoie `KayaBot`, sauf si + `tvmaze_user_agent` ou `KAYABOT_TVMAZE_USER_AGENT` en définit un autre. +- Langue et région : aucune. +- Ce que KayaBot conserve : le nom de la série, l'année tirée de `premiered`, + et le `score` du résultat de recherche (le score propre de la série quand il + vaut 0) comme note de la source. +- Quota : au moins 20 appels toutes les 10 secondes par adresse IP. +- Conditions : l'[API](https://www.tvmaze.com/api) est sous licence + [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) : TVmaze doit + être cité comme source, et les données partagées suivent les conditions + ShareAlike (partage dans les mêmes conditions). + +### OMDb + +- Hôte : `https://www.omdbapi.com`. +- Requêtes : `?s=<titre>` pour chercher et `?i=<identifiant IMDb>` pour les + détails d'un film. +- Authentification : la clé part dans le paramètre de requête `apikey`. +- Langue et région : aucune. +- Ce que KayaBot conserve : le titre, les quatre premiers chiffres de `Year`, + et l'identifiant IMDb, qui sert aussi d'identifiant au résultat. OMDb ne + fournit aucune note que KayaBot utilise : chaque résultat reçoit une note de + source fixe de 0.7. +- Quota : une clé gratuite permet 1 000 requêtes par jour + ([clés API](https://www.omdbapi.com/apikey.aspx)). +- Conditions : le contenu d'OMDb est sous licence + [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/), et OMDb + n'est ni approuvé par IMDb.com ni affilié à IMDb.com + ([conditions](https://www.omdbapi.com/legal.htm)). + +## Limites et erreurs + +Chaque requête a un délai d'expiration de 10 secondes. Une réponse +`429 Too Many Requests` est signalée comme une limite de débit, et une source +qui expire ou échoue est écartée de cette recherche sans arrêter les autres. +Les messages d'erreur n'incluent jamais l'URL de la requête : une clé envoyée +dans la chaîne de requête n'atteint donc pas la fenêtre. + +Les quotas et les conditions changent : consultez les pages de chaque service +avant de vous fier à un chiffre. diff --git a/scripts/i18n-check.sh b/scripts/i18n-check.sh new file mode 100755 index 0000000..e603e2c --- /dev/null +++ b/scripts/i18n-check.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +# Whether every translated document still matches the English it was made from. +# +# Each translation carries the BLOB HASH of its source, not a commit sha: +# +# <!-- eidos-i18n: source=docs/guide/install.md sha=62a0541b --> +# +# Translations live at docs/i18n/<lang>/<the source path>, so a translation is +# a mirror of the repo root: docs/i18n/fr/docs/guide/install.md translates +# docs/guide/install.md. That shape is what makes a link between two translated +# pages the SAME string as the link between their English originals. +# +# The hash is `git hash-object <source>`, computable from the working tree with +# no history at all. That is deliberate. The obvious design - record the last +# commit that touched the source and compare with `git log` - needs history this +# repo's CI does not fetch: `actions/checkout` clones with depth 1 unless told +# otherwise, `git log` on a shallow clone reports one commit, every comparison +# comes out equal, and the check passes green forever while translations rot. +# A freshness check that cannot fail is worse than none: it is a badge asserting +# something nobody verified. +# +# Exit 1 if any translation is stale or malformed. `--fix` restamps instead, +# for when you have just re-translated. +set -uo pipefail +cd "$(dirname "$0")/.." + +fix=0 +stamp=0 +case "${1:-}" in + --fix) fix=1 ;; + # Adds a missing header to a translated file. An authoring convenience, kept + # separate from --fix on purpose: CI must keep FAILING on an unstamped file, + # because a translation with no stamp is one nothing can ever tell is stale. + --stamp) stamp=1 ;; +esac + +stale=0 +missing=0 +checked=0 + +# A translation's source is its path with the docs/i18n/<lang>/ prefix removed. +while IFS= read -r t; do + checked=$((checked + 1)) + # Strip `find`'s leading `./`: the stamp inside a file names its source the + # way a human would write it, repo-relative, and a path that does not match + # the stamp byte for byte makes --fix silently substitute nothing. + t="${t#./}" + src="$(printf '%s' "$t" | sed -E 's|^docs/i18n/[^/]+/||')" + if [ ! -f "$src" ]; then + printf 'ORPHAN %s\n its English source %s does not exist\n' "$t" "$src" + missing=$((missing + 1)) + continue + fi + want="$(git hash-object "$src")" + have="$(sed -n 's/.*eidos-i18n: source=[^ ]* sha=\([0-9a-f]*\).*/\1/p' "$t" | head -1)" + if [ -z "$have" ]; then + if [ "$stamp" = 1 ]; then + printf '<!-- eidos-i18n: source=%s sha=%s -->\n\n%s\n' "$src" "$want" "$(cat "$t")" > "$t.tmp" + mv "$t.tmp" "$t" + printf 'STAMPED %s\n' "$t" + continue + fi + printf 'NO STAMP %s\n add: <!-- eidos-i18n: source=%s sha=%s -->\n' "$t" "$src" "$want" + missing=$((missing + 1)) + continue + fi + if [ "$have" != "$want" ]; then + if [ "$fix" = 1 ]; then + sed -i "s|eidos-i18n: source=$src sha=$have|eidos-i18n: source=$src sha=$want|" "$t" + # Confirm the substitution LANDED before saying so. The first version + # of this script printed RESTAMPED unconditionally, and because a + # path mismatch made the pattern match nothing, it announced success + # while changing not one byte - the exact failure this whole check + # exists to prevent, inside the check itself. + if [ "$(sed -n 's/.*eidos-i18n: source=[^ ]* sha=\([0-9a-f]*\).*/\1/p' "$t" | head -1)" = "$want" ]; then + printf 'RESTAMPED %s\n' "$t" + else + printf 'COULD NOT RESTAMP %s\n its stamp does not name %s\n' "$t" "$src" + stale=$((stale + 1)) + fi + else + n="$(git log --oneline "$src" 2>/dev/null | wc -l)" + printf 'STALE %s\n %s changed since this was translated (stamped %s, now %s)\n' \ + "$t" "$src" "${have:0:8}" "${want:0:8}" + [ "$n" -gt 1 ] && printf ' history: %s commits on the source\n' "$n" + stale=$((stale + 1)) + fi + fi +done < <(find docs/i18n -name '*.md' 2>/dev/null | sort) + +# Structure. Nobody here reads fifteen languages, so the honest check on a +# translation's BODY is not its prose but its shape: a page with the source's +# headings, code blocks and table rows is a page somebody actually translated, +# and one with half of them is a page that was summarised, truncated, or had its +# examples "helpfully" merged. This catches the failure a reviewer cannot. +shape() { + # headings | fenced code blocks | table rows, for one file + printf '%s %s %s' \ + "$(grep -c '^#\{1,6\} ' "$1")" \ + "$(grep -c '^```' "$1")" \ + "$(grep -c '^|' "$1")" +} + +for t in $(find docs/i18n -name '*.md' 2>/dev/null | sort); do + t="${t#./}" + src="$(printf '%s' "$t" | sed -E 's|^docs/i18n/[^/]+/||')" + [ -f "$src" ] || continue + # The stamp line is one extra line in the translation and nothing else. + a="$(shape "$src")" + b="$(shape "$t")" + if [ "$a" != "$b" ]; then + printf 'SHAPE %s\n headings/code-fences/table-rows: source %s, translation %s\n' \ + "$t" "$a" "$b" + stale=$((stale + 1)) + fi +done + +printf '\n%s translation(s) checked' "$checked" +[ "$stale" -gt 0 ] && printf ', %s STALE' "$stale" +[ "$missing" -gt 0 ] && printf ', %s unusable' "$missing" +printf '\n' + +if [ "$stale" -gt 0 ] || [ "$missing" -gt 0 ]; then + cat <<'MSG' + +A stale translation is worse than a missing one: it looks authoritative and +gives last month's instructions. Either re-translate it and run this with +--fix, or delete it and let the reader fall back to English. +MSG + exit 1 +fi +exit 0 diff --git a/scripts/i18n-links.py b/scripts/i18n-links.py new file mode 100755 index 0000000..e036a55 --- /dev/null +++ b/scripts/i18n-links.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Every markdown link and anchor in the docs resolves. + +Split from i18n-check.sh because it needs a real slug function: an anchor is +GitHub's slug of a heading, and a translated page's headings are - obviously - +not the English ones. The failure this exists to catch is quiet and specific: +a translation that correctly rewrote `usage.md` to `usage.de.md` and correctly +left `#why-passthrough-is-off-by-default` alone, producing a link that lands on +the right page at the wrong place, or nowhere at all. Twenty-nine of those +shipped in one batch before this existed. +""" +import glob +import os +import re +import sys + +ANCHOR = re.compile(r"\]\(([^)\s#]*\.md)(?:#([^)\s]+))?\)") + + +def slug(heading: str) -> str: + s = heading.strip().lower() + s = re.sub(r"`([^`]*)`", r"\1", s) + s = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", s) + s = re.sub(r"[*_]", "", s) + s = "".join(c for c in s if c.isalnum() or c in " -_") + return s.strip().replace(" ", "-") + + +def headings(path: str) -> set: + with open(path, encoding="utf-8") as fh: + return {slug(l.lstrip("#").strip()) for l in fh if l.startswith("#")} + + +def main() -> int: + bad = 0 + files = sorted(f for f in glob.glob("**/*.md", recursive=True) if not f.startswith("target/")) + for f in files: + d = os.path.dirname(f) + with open(f, encoding="utf-8") as fh: + text = fh.read() + for m in ANCHOR.finditer(text): + target, anchor = m.group(1), m.group(2) + if target.startswith("http"): + continue + p = os.path.normpath(os.path.join(d, target)) + if not os.path.exists(p): + print(f"MISSING FILE {f}\n -> {target}") + bad += 1 + continue + if anchor and anchor not in headings(p): + print(f"MISSING ANCHOR {f}\n -> {target}#{anchor}") + bad += 1 + print(f"\n{len(files)} markdown file(s) checked", end="") + print(f", {bad} broken link(s)" if bad else ", every link resolves") + return 1 if bad else 0 + + +if __name__ == "__main__": + sys.exit(main())