No description
  • HTML 73.2%
  • Svelte 10%
  • TypeScript 7.4%
  • JavaScript 6.9%
  • CSS 2.3%
  • Other 0.1%
Find a file
RiasGFirst 51e39741de
All checks were successful
docker / build (push) Successful in 12m19s
feat(admin): distinguer « à tagger » et « sans média » par subreddit
Le compteur « à tagger » prenait tous les posts non taggés, y compris ceux
dont le média n'est pas sur disque — que le worker ne peut pas recevoir, d'où
un écart avec son « en attente ». Utilise untagged_with_media de RedCrawl
(GET /subreddits?media_counts=true, repli sur l'ancien calcul s'il manque) et
affiche le refresh automatique « en cours » plutôt qu'un compte à rebours
dépassé.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HPYnXHGUAU7atRhZfZg2uX
2026-10-03 14:41:00 +02:00
.forgejo/workflows build(docker): forcer l'image en linux/amd64 depuis un runner ARM 2026-08-29 22:23:20 +02:00
comm docs: dossier comm/ et vidéo de présentation dans le README 2026-10-03 14:21:15 +02:00
deploy feat: full Webapp v0.0.1 2026-08-29 00:17:22 +02:00
design feat: full Webapp v0.0.1 2026-08-29 00:17:22 +02:00
migrations feat: full Webapp v0.0.1 2026-08-29 00:17:22 +02:00
scripts fix(upload): relever BODY_SIZE_LIMIT pour autoriser les gros fichiers 2026-09-29 22:29:09 +02:00
src feat(admin): distinguer « à tagger » et « sans média » par subreddit 2026-10-03 14:41:00 +02:00
static feat: full Webapp v0.0.1 2026-08-29 00:17:22 +02:00
.dockerignore feat: full Webapp v0.0.1 2026-08-29 00:17:22 +02:00
.env.example feat(auth): page de login façon RiasDrive + connexion développeur 2026-10-03 13:07:17 +02:00
.gitignore docs: dossier comm/ et vidéo de présentation dans le README 2026-10-03 14:21:15 +02:00
CLAUDE.md feat(admin): distinguer « à tagger » et « sans média » par subreddit 2026-10-03 14:41:00 +02:00
docker-compose.yml feat: full Webapp v0.0.1 2026-08-29 00:17:22 +02:00
docker-entrypoint.sh fix(upload): relever BODY_SIZE_LIMIT pour autoriser les gros fichiers 2026-09-29 22:29:09 +02:00
Dockerfile build(docker): forcer l'image en linux/amd64 depuis un runner ARM 2026-08-29 22:23:20 +02:00
package-lock.json feat: full Webapp v0.0.1 2026-08-29 00:17:22 +02:00
package.json fix(upload): relever BODY_SIZE_LIMIT pour autoriser les gros fichiers 2026-09-29 22:29:09 +02:00
PLAN.md feat: déléguer le tagging à un worker distant 2026-09-21 22:24:46 +02:00
README.md docs: dossier comm/ et vidéo de présentation dans le README 2026-10-03 14:21:15 +02:00
svelte.config.js feat: full Webapp v0.0.1 2026-08-29 00:17:22 +02:00
tsconfig.json feat: full Webapp v0.0.1 2026-08-29 00:17:22 +02:00
vite.config.ts feat: full Webapp v0.0.1 2026-08-29 00:17:22 +02:00

riasbooru

Ta collection. Ton serveur. — un booru self-hosted pour les images collectées par RedCrawl, derrière un login PocketID (OpenID Connect). 18+.

Vidéo de présentation de riasbooru

▶ Voir la vidéo de présentation (25 s, médias floutés) — autres supports dans comm/.

En bref

  • Recherche booru — tag -tag rating:e score:>50 ai:true order:top, avec autocomplétion des tags et filtres partageables par URL.
  • Demandes de contenu — un subreddit entier ou un post Reddit précis ; riasbooru pilote RedCrawl (scrape → téléchargement → tagging) jusqu'au bout.
  • Uploads — envoi de fichiers, tagués automatiquement côté RedCrawl.
  • Review admin — demandes et uploads passent par une file de modération, avec acceptation automatique au bout de 24 h.
  • Favoris, diaporama aléatoire, navigation clavier j/k dans le détail.
  • Self-hosted — une image Docker, une base SQLite, login PocketID.

riasbooru ne stocke ni posts, ni tags, ni médias : il consomme l'API HTTP de RedCrawl et n'ajoute par-dessus que l'état de gouvernance (file de review, favoris, recherches sauvegardées).

Voir PLAN.md pour la conception détaillée.


Stack

Élément Choix
App SvelteKit 2 (adapter-node), Svelte 5, TypeScript
UI Tailwind CSS 4
DB (gouvernance only) SQLite via node:sqlite + migrations SQL
Tâches de fond sweeper + orchestrateur in-process
Auth openid-client v6 (Authorization Code + PKCE)
Packaging une image Docker multi-stage, volume /data

Écart au plan : le plan prévoyait better-sqlite3. On utilise le module node:sqlite intégré à Node ≥ 24 : mêmes fonctionnalités pour cet usage, mais aucun module natif à compiler (build Docker plus simple et plus rapide). Conséquence : Node 24 minimum.


Démarrage en développement

npm install
cp .env.example .env      # puis remplir (voir « Configuration »)
npm run dev               # http://localhost:5173

En dev, RedCrawl est joignable à la même adresse depuis le serveur et depuis le navigateur :

APP_URL=http://localhost:5173
UPSTREAM_INTERNAL_URL=http://127.0.0.1:8000
UPSTREAM_PUBLIC_URL=http://127.0.0.1:8000
DATA_DIR=./data

Autres commandes :

npm run check      # svelte-check (types + templates)
npm run build      # build de production dans build/
npm run start      # sert le build sur le port 3000

Ports

npm run dev et npm run start n'écoutent pas le même port, et APP_URL doit correspondre à celui qui tourne (c'est lui qui construit le redirect_uri OIDC) :

Commande Serveur Port
npm run dev Vite 5173
npm run start adapter-node 3000 (PORT pour surcharger)
Docker adapter-node 3000 dans le conteneur

Si tu alternes entre les deux, déclare les deux callbacks dans PocketID.

Chargement du .env

Vite lit .env automatiquement en dev. build/index.js non : il ne voit que l'environnement du shell. npm run start passe donc par scripts/start.js, lancé avec --env-file-if-exists=.env.

Si tu lances node build/index.js directement, il faut exporter les variables toi-même — sinon la connexion échoue sur Variable d'environnement manquante : OIDC_ISSUER.


Configuration

Toutes les variables sont lues à l'exécution : la même image Docker fonctionne avec des .env différents. Référence complète dans .env.example.

Variable Rôle
APP_URL URL publique. Sert au redirect_uri OIDC et à ORIGIN.
OIDC_ISSUER / OIDC_CLIENT_ID / OIDC_CLIENT_SECRET Client PocketID.
OIDC_SCOPE Défaut openid profile email groups. Le scope groups est requis pour ADMIN_GROUP.
SESSION_SECRET ≥ 32 caractères. Le changer invalide les sessions.
UPSTREAM_INTERNAL_URL server → RedCrawl (posts, tags, jobs).
UPSTREAM_PUBLIC_URL navigateur → RedCrawl (/media/...). Vide si same-origin.
UPSTREAM_API_KEY Doit correspondre à API_KEY côté RedCrawl.
ADMIN_GROUP Valeur attendue dans le claim groups. Défaut admins.
REQUEST_AUTO_ACCEPT_HOURS Délai avant auto-acceptation. Défaut 24.
MAX_UPLOAD_MB À garder ≤ MAX_CUSTOM_UPLOAD_MB de RedCrawl (défaut 25).
DATA_DIR SQLite + uploads en attente. /data dans l'image.

ORIGIN (important)

adapter-node compare l'en-tête Origin des requêtes POST à la variable ORIGIN. Sans elle, tous les envois de formulaire et les uploads sont rejetés en 403. Elle est dérivée automatiquement de APP_URL, par docker-entrypoint.sh dans l'image et par scripts/start.js via npm run start. Il n'y a donc rien à faire, sauf si tu lances node build/index.js à la main.


Configurer le client PocketID

  1. Dans PocketID, créer un OIDC Client nommé riasbooru.
  2. Callback URL : ${APP_URL}/auth/callback (ex. https://booru.example.com/auth/callback).
  3. Relever le client ID et le client secret → OIDC_CLIENT_ID / OIDC_CLIENT_SECRET.
  4. OIDC_ISSUER = l'URL racine de PocketID (la discovery est lue sur ${OIDC_ISSUER}/.well-known/openid-configuration).
  5. Créer un groupe d'administrateurs et y placer les comptes concernés. Reporter son nom exact dans ADMIN_GROUP.
  6. S'assurer que le client expose bien le claim groups (scope groups). riasbooru lit ce claim dans l'ID token et dans /userinfo ; il suffit qu'il soit présent dans l'un des deux.

Toute l'application est derrière le login : un visiteur anonyme est redirigé vers PocketID, et les routes /api/* répondent 401.


Docker

Image seule

docker build -t riasbooru:latest .
docker run -d --name riasbooru -p 3000:3000 \
  --env-file .env -v riasbooru-data:/data \
  riasbooru:latest

L'image tourne en utilisateur non-root (node), expose /healthz pour le HEALTHCHECK, et applique les migrations SQLite au démarrage avant de servir la première requête.

Build automatique (Forgejo Actions)

.forgejo/workflows/docker.yml construit l'image et la pousse vers le registre de conteneurs de l'instance Forgejo. Il se déclenche sur un push main, un push de tag git v*, ou manuellement (workflow_dispatch).

Contexte Tags produits
Toujours :sha-<7>
Push sur main :latest
Push d'un tag vX.Y.Z :vX.Y.Z, :latest
Push d'une autre branche :<branche> (/ → -)

Prérequis : un forgejo-runner enregistré et les Actions activées sur le dépôt (Settings → Repository). Le job tourne dans catthehacker/ubuntu:act-latest (le mapping par défaut de ubuntu-latest sur un runner Forgejo est une image nue sans node/git/docker) et met en cache les couches dans le registre sous le tag :buildcache.

Variables et secrets de dépôt, tous optionnels (Settings → Actions) :

Nom Type Rôle
REGISTRY_HOST variable Cibler un registre autre que l'instance Forgejo.
REGISTRY_USER variable Compte de login si REGISTRY_TOKEN appartient à un autre compte que l'auteur du push.
REGISTRY_TOKEN secret PAT write:package, si le token intégré au workflow ne peut pas pousser de paquets (403).

Stack complète

docker-compose.yml démarre riasbooru, redcrawl et redcrawl-postgres sur un réseau commun :

cp .env.example .env      # + REDDIT_COOKIES pour RedCrawl
docker compose up -d
  • UPSTREAM_INTERNAL_URL=http://redcrawl:8000 — nom de service, réseau interne.
  • UPSTREAM_PUBLIC_URL — doit être joignable depuis le navigateur : le port 8000 de RedCrawl est publié pour ça, car les images et vidéos sont chargées directement depuis RedCrawl avec ?api_key=.

Profils optionnels :

docker compose --profile pocketid up -d   # ajoute un provider OIDC
docker compose --profile proxy up -d      # Caddy en origine unique

Avec le profil proxy (deploy/Caddyfile), /media/* est routé vers RedCrawl sur la même origine que riasbooru : UPSTREAM_PUBLIC_URL peut alors rester vide et le port 8000 ne plus être publié.

Pour tester avec un RedCrawl externe déjà en place, ne démarrer que riasbooru et pointer les deux URLs upstream vers cette instance :

docker compose up -d riasbooru

Une seule instance

Le sweeper et l'orchestrateur tournent dans le process applicatif. Ne pas scaler horizontalement : deux instances doubleraient les jobs soumis à RedCrawl.


Recherche

Barre unique, syntaxe booru :

1girl smile -monochrome rating:explicit score:>50 ai:true order:top
Token Paramètre RedCrawl
tag tag[]
-tag exclude_tag[]
rating:explicit (ou rating:e) rating
score:>50, score:50 min_score
ai:true / ai:false is_ai
order:top / order:new sort
untagged:true tagged_only=false

Les posts non taggés sont exclus par défaut ; la case « inclure les posts non taggés » de la galerie ajoute untagged:true. L'autocomplétion (GET /tags, 20 premiers) se déclenche sur le token sous le curseur ; Tab ou Entrée complète, une seconde Entrée lance la recherche.


Pages

Route Contenu
/ Galerie : grille, scroll infini, recherche + autocomplétion
/{sub}/{id} Détail : carrousel + <video>, tags, métadonnées, badge IA, nav j/k
/random Diaporama plein écran de posts aléatoires (« Rebrasser » pour un nouveau tirage)
/favorites Mes favoris
/requests Mes demandes : contenu Reddit et upload de fichier (deux onglets), historique unifié
/upload Redirige (308) vers /requests?mode=fichier — l'upload a fusionné dans Demandes
/admin File de review (demandes + uploads) — admin uniquement

La navigation j / k du détail a besoin du contexte de recherche : les liens de la galerie transportent la requête (?q=) et le rang du post (?o=). En arrivant directement sur une URL de détail, il n'y a pas de voisins.


API interne

Toutes ces routes exigent une session ; les actions de review exigent en plus isAdmin.

Route Méthode Rôle
/api/me GET utilisateur courant
/api/posts GET ?q=&limit=&offset= — scroll infini
/api/posts/{sub}/{id} GET un post
/api/tags GET ?prefix= — autocomplétion
/api/requests GET, POST mes demandes (?scope=all si admin) / créer {target}
/api/requests/{id} POST {action:'approve'|'reject', note?} — admin
/api/uploads GET, POST mes uploads / envoyer (multipart)
/api/uploads/{id} POST {action:'approve'|'reject', note?} — admin
/api/jobs/{id} GET statut d'un job RedCrawl
/healthz GET sonde (public)

POST /api/requests et POST /api/uploads sont limités en débit par utilisateur (5 demandes/h, 10 uploads/h).


Cycle de vie

Demandes — un seul formulaire, une seule file de review, deux cibles possibles. POST /api/requests prend un champ target unique : un lien de post Reddit donne une demande de post, tout le reste est lu comme un nom de subreddit — y compris une URL de subreddit collée telle quelle.

Saisie Interprétation
nomdusub, r/nomdusub, https://reddit.com/r/nomdusub/ subreddit
https://www.reddit.com/r/sub/comments/<id>/titre/ post <id>
/r/sub/comments/<id>/titre/ (permalink nu) post <id>
https://redd.it/<id> post <id>
https://www.reddit.com/r/sub/s/<id> (lien de partage) post, id résolu par RedCrawl

La query string est retirée avant stockage, et deux URLs qui désignent le même post (permalink, redd.it) se dédupliquent sur l'id extrait. Les liens de partage, dont l'id n'est connu qu'après résolution, ne se dédupliquent que sur l'URL exacte.

Cycle : pending → (admin accepte / refuse, ou auto-acceptation après 24 h) → traitement, qui diffère selon la cible :

  • subreddit — orchestration scrape → download → tag → done. RedCrawl n'enchaîne pas ces étapes lui-même : riasbooru soumet chaque job et poll son statut (tick de 30 s). Un sub déjà connu est re-scrapé quand même (refresh incrémental, force=false).
  • post — une seule phase post : POST /jobs/post fait déjà add→download→tag côté RedCrawl. À la fin, <sub>/<id> du post importé est enregistré et la page /requests propose un lien direct vers lui.

Upload — le fichier est écrit dans DATA_DIR/pending_uploads/, la ligne passe pending. À l'acceptation, il est envoyé à POST /jobs/custom puis supprimé localement ; au refus, il est supprimé sans être envoyé. Si l'envoi échoue, le fichier est conservé et le sweeper réessaie.


Limites connues

  • Les jobs RedCrawl vivent en mémoire : après un redémarrage de RedCrawl, un job_id devient inconnu. L'orchestrateur détecte le 404 et relance la phase courante.
  • UPSTREAM_API_KEY se retrouve dans le HTML servi au navigateur (décision actée : pas de proxy média, PLAN.md §3.1). La clé ne doit donner accès qu'à RedCrawl.
  • Le formulaire d'upload n'a pas d'autocomplétion de tags : POST /jobs/custom n'accepte que file et title, le tagging est fait par WD14 côté RedCrawl.
  • Le design est provisoire — l'esquisse Claude Design n'a pas encore été intégrée (PLAN.md §12, étape 8).