- HTML 73.2%
- Svelte 10%
- TypeScript 7.4%
- JavaScript 6.9%
- CSS 2.3%
- Other 0.1%
|
All checks were successful
docker / build (push) Successful in 12m19s
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 |
||
|---|---|---|
| .forgejo/workflows | ||
| comm | ||
| deploy | ||
| design | ||
| migrations | ||
| scripts | ||
| src | ||
| static | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| CLAUDE.md | ||
| docker-compose.yml | ||
| docker-entrypoint.sh | ||
| Dockerfile | ||
| package-lock.json | ||
| package.json | ||
| PLAN.md | ||
| README.md | ||
| svelte.config.js | ||
| tsconfig.json | ||
| vite.config.ts | ||
riasbooru
Ta collection. Ton serveur. — un booru self-hosted pour les images collectées par RedCrawl, derrière un login PocketID (OpenID Connect). 18+.
▶ 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/kdans 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 modulenode:sqliteinté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
- Dans PocketID, créer un OIDC Client nommé
riasbooru. - Callback URL :
${APP_URL}/auth/callback(ex.https://booru.example.com/auth/callback). - Relever le client ID et le client secret →
OIDC_CLIENT_ID/OIDC_CLIENT_SECRET. OIDC_ISSUER= l'URL racine de PocketID (la discovery est lue sur${OIDC_ISSUER}/.well-known/openid-configuration).- Créer un groupe d'administrateurs et y placer les comptes concernés.
Reporter son nom exact dans
ADMIN_GROUP. - S'assurer que le client expose bien le claim
groups(scopegroups). 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/postfait déjà add→download→tag côté RedCrawl. À la fin,<sub>/<id>du post importé est enregistré et la page/requestspropose 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_iddevient inconnu. L'orchestrateur détecte le 404 et relance la phase courante. UPSTREAM_API_KEYse 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/customn'accepte quefileettitle, 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).
