Aller au contenu

API HTTP

Romule expose une API petite et stable, pour interroger ta ludothèque depuis un tableau de bord, un script ou une tâche planifiée — sans navigateur et sans session.

Télécharger la spécification OpenAPI 3.1

La promesse, précisément

Dans une même version majeure :

  • aucune route ne disparaît ;
  • aucun champ existant ne change de nom ni de type ;
  • des champs peuvent apparaître — ignore donc ceux que tu ne connais pas.

C'est toute la promesse. Romule sert une centaine d'autres routes /api/... ; elles existent pour sa propre interface, elles changent quand un écran change, et elles ne sont pas couvertes. Si tu construis dessus, attends-toi à ce qu'elles bougent.

Pourquoi une surface à part

Geler les routes de l'interface reviendrait à geler l'interface. L'API publique est délibérément plus petite que ce dont l'application se sert, pour la même raison que Sonarr et Radarr publient une surface choisie plutôt que leurs entrailles.

Obtenir une clé

Chaque requête a besoin d'une clé d'API. Les clés sont nommées, révocables une par une, et montrent leur dernière utilisation.

Depuis l'interface — Réglages → Accès → Clés d'API. Donne-lui un nom qui dise à quoi elle sert (tableau-de-bord, sauvegarde-nuit) : ce nom est ce qui te permettra plus tard de savoir laquelle révoquer.

En ligne de commande — utile dans un conteneur, où il n'y a parfois aucun navigateur :

romule apikey create tableau-de-bord
# sous Docker :
docker compose exec romule python3 -m romule apikey create tableau-de-bord
Cle creee : tableau-de-bord

  rml_Ac0ffee1S3cr3t...

Note-la maintenant : elle n'est conservee que sous forme
d'empreinte et ne pourra pas etre reaffichee.

La sortie est sans accents

Elle est reproduite telle quelle. Comme le rapport d'audit et les pages de connexion, la ligne de commande est composée par le serveur et ne passe jamais par le catalogue de traduction de l'interface — voir Fonctions bêta.

Montrée une fois, et une seule

Romule range une empreinte SHA-256 de la clé, jamais la clé elle-même. C'est ce qui rend une fuite de son fichier d'état inoffensive — et c'est aussi pourquoi la clé ne peut plus jamais être réaffichée. Note-la à la création.

romule apikey list montre les clés, leur préfixe et leur dernier usage. romule apikey revoke <id> en retire une. Une clé révoquée reste listée, pour que tu puisses encore répondre à « est-ce que cette clé a servi après que je l'ai retirée ? ».

S'authentifier

Envoie la clé dans un en-tête :

curl -H "X-Api-Key: rml_..." http://localhost:8787/api/v1/stats

Un paramètre d'URL marche aussi, pour les clients à qui l'on ne peut donner qu'une adresse — une tuile de tableau de bord, un wget dans un cron :

curl "http://localhost:8787/api/v1/stats?apikey=rml_..."

L'en-tête vaut mieux

Une URL finit dans les journaux du proxy, dans l'historique du navigateur et dans celui du shell. N'utilise le paramètre que si un en-tête est réellement impossible.

Ce qu'une clé peut atteindre

Une clé atteint /api/v1/ et rien d'autre. Elle ne peut ni ouvrir l'interface, ni lire la configuration, ni toucher aux comptes.

C'est plus strict qu'il n'y paraît : présenter une clé ne donne pas des droits, cela choisit un régime. Une requête venue de 127.0.0.1 obtient normalement tous les droits locaux — mais dès qu'elle porte X-Api-Key, c'est la clé qui décide, et la clé est portée. Une clé ne peut donc jamais élargir un accès : au mieux, elle le restreint.

CSRF

Romule refuse les requêtes qui modifient l'état et annoncent une origine étrangère. Un client en ligne de commande n'envoie aucun en-tête Origin et est accepté — c'est la clé qui protège ces routes, et un navigateur ne l'attache jamais tout seul comme il attache un cookie.

Les routes

L'adresse de base est ton instance, http://localhost:8787 ci-dessous.

Lire la ludothèque

Route Ce qu'elle donne
GET /api/v1/health vivant — c'est aussi la sonde du conteneur
GET /api/v1/system version, licence, source, temps de fonctionnement
GET /api/v1/stats décomptes et taille totale
GET /api/v1/library l'inventaire, paginé
GET /api/v1/library/{cle} un jeu
GET /api/v1/search?q= recherche par nom ou par title ID
GET /api/v1/platforms plateformes configurées
GET /api/v1/device état de la console connectée
GET /api/v1/job la tâche en cours, s'il y en a une
GET /api/v1/trash ce qui est encore restaurable
curl -H "X-Api-Key: $CLE" http://localhost:8787/api/v1/stats
{ "total": 412, "base": 180, "update": 150, "dlc": 82,
  "bytes": 174929203200, "to_convert": 3 }

La clé d'un jeu est son chemin relatif à la ludothèque — le même identifiant que celui de l'interface. Elle contient des espaces et des crochets : encode-la.

curl -H "X-Api-Key: $CLE" \
  "http://localhost:8787/api/v1/library/GAMES%2FUn%20Jeu%20%5B0100ABC%5D.nsp"

Aucun champ ne contient de chemin absolu. Il n'apprendrait rien à un client et révélerait l'arborescence du serveur — souvent, le nom de compte de quelqu'un.

Lancer une tâche

Route
POST /api/v1/scan relire la ludothèque
POST /api/v1/convert convertir les .nsz / .xcz restants
POST /api/v1/push envoyer les jeux en attente vers la console
curl -X POST -H "X-Api-Key: $CLE" http://localhost:8787/api/v1/scan

Romule exécute une tâche à la fois — il n'y a pas de file d'attente, et l'API le dit plutôt que de faire semblant. Un démarrage rend 202 ; si quelque chose tourne déjà, tu reçois 409, et réessayer plus tard est la bonne réponse.

Suis l'avancement avec GET /api/v1/job :

{ "running": true, "label": "convert", "done": 2, "total": 5,
  "detail": "Un Jeu.nsz" }

Pagination

GET /api/v1/library et GET /api/v1/search acceptent page (à partir de 1) et limit (50 par défaut, 200 au maximum), et répondent avec la page et son contexte :

{ "page": 2, "limit": 50, "total": 412, "pages": 9, "items": [  ] }

Deux comportements, volontairement différents :

  • une valeur illisible (page=zero) retombe sur le défaut — le client s'est trompé de type, il n'y a rien à en tirer ;
  • une valeur hors bornes (limit=100000, limit=-4) est ramenée dans les bornes — l'intention est claire, et la refuser obligerait chaque client à connaître le plafond avant de demander.

Une page au-delà de la fin rend un items vide, pas une erreur.

Erreurs

Code
400 un paramètre obligatoire manque
401 / 403 pas de clé, clé inconnue, clé révoquée, ou hors de portée
404 route inconnue, ou clé de jeu inconnue
409 une tâche tourne déjà
429 débit limité — Retry-After dit combien de temps
500 quelque chose a échoué côté serveur

Les corps d'erreur portent un error stable et un message lisible :

{ "error": "busy", "message": "Another task is already running." }

Le message ne contient jamais de chemin du serveur : une panne interne est rendue comme internal_error avec une phrase générique, et le détail va dans le journal de Romule.

Un exemple complet

Me prévenir quand plus de trois fichiers restent à convertir :

#!/bin/sh
CLE=rml_...
N=$(curl -fsS -H "X-Api-Key: $CLE" http://localhost:8787/api/v1/stats \
    | python3 -c 'import json,sys; print(json.load(sys.stdin)["to_convert"])')
[ "$N" -gt 3 ] && echo "$N fichiers à convertir" && \
  curl -fsS -X POST -H "X-Api-Key: $CLE" http://localhost:8787/api/v1/convert