Remove Meilisearch dependency entirely. Search is now handled by PostgreSQL ILIKE with pg_trgm indexes, joining series_metadata for series-level authors. No external search engine needed. - Replace search.rs Meilisearch HTTP calls with PostgreSQL queries - Remove meili.rs from indexer, sync_meili call from job pipeline - Remove MEILI_URL/MEILI_MASTER_KEY from config, state, env files - Remove meilisearch service from docker-compose.yml - Add migration 0027: drop sync_metadata, enable pg_trgm, add indexes - Remove search resync button/endpoint (no longer needed) - Update all documentation (CLAUDE.md, README.md, AGENTS.md, PLAN.md) API contract unchanged — same SearchResponse shape returned. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
308 lines
8.6 KiB
Markdown
308 lines
8.6 KiB
Markdown
# AGENTS.md - Agent Coding Guidelines for Stripstream Librarian
|
|
|
|
This file provides guidelines for agentic coding agents operating in this repository.
|
|
|
|
---
|
|
|
|
## 1. Build, Lint, and Test Commands
|
|
|
|
### Build Commands
|
|
|
|
```bash
|
|
# Build debug version (fastest for development)
|
|
cargo build
|
|
|
|
# Build release version (optimized)
|
|
cargo build --release
|
|
|
|
# Build specific crate
|
|
cargo build -p api
|
|
cargo build -p indexer
|
|
|
|
# Watch mode for development (requires cargo-watch)
|
|
cargo watch -x build
|
|
```
|
|
|
|
### Lint & Format Commands
|
|
|
|
```bash
|
|
# Run clippy lints
|
|
cargo clippy
|
|
|
|
# Fix auto-fixable clippy warnings
|
|
cargo clippy --fix
|
|
|
|
# Format code
|
|
cargo fmt
|
|
|
|
# Check formatting without making changes
|
|
cargo fmt -- --check
|
|
```
|
|
|
|
### Test Commands
|
|
|
|
```bash
|
|
# Run all tests
|
|
cargo test
|
|
|
|
# Run tests for specific crate
|
|
cargo test -p api
|
|
cargo test -p indexer
|
|
cargo test -p parsers
|
|
|
|
# Run a single test by name
|
|
cargo test test_name_here
|
|
|
|
# Run tests with output display
|
|
cargo test -- --nocapture
|
|
|
|
# Run doc tests
|
|
cargo test --doc
|
|
```
|
|
|
|
### Database Migrations
|
|
|
|
```bash
|
|
# Run migrations manually (via sqlx CLI)
|
|
# Ensure DATABASE_URL is set, then:
|
|
sqlx migrate run
|
|
|
|
# Create new migration
|
|
sqlx migrate add -r migration_name
|
|
```
|
|
|
|
### Docker Development
|
|
|
|
`docker-compose.yml` est à la **racine** du projet (pas dans `infra/`).
|
|
|
|
```bash
|
|
# Start infrastructure only
|
|
docker compose up -d postgres
|
|
|
|
# Start full stack
|
|
docker compose up -d
|
|
|
|
# View logs
|
|
docker compose logs -f api
|
|
docker compose logs -f indexer
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Code Style Guidelines
|
|
|
|
### General Principles
|
|
|
|
- **Conciseness**: Keep responses short and direct. Avoid unnecessary preamble or explanation.
|
|
- **Idiomatic Rust**: Follow Rust best practices and ecosystem conventions.
|
|
- **Error Handling**: Use `anyhow::Result<T>` for application code, `std::io::Result<T>` for simple file operations.
|
|
- **Async**: Use `tokio` for async runtime. Prefer `#[tokio::main]` over manual runtime.
|
|
|
|
### Naming Conventions
|
|
|
|
| Element | Convention | Example |
|
|
|---------|------------|---------|
|
|
| Variables | snake_case | `let book_id = ...` |
|
|
| Functions | snake_case | `fn get_book(...)` |
|
|
| Structs/Enums | PascalCase | `struct BookItem` |
|
|
| Modules | snake_case | `mod books;` |
|
|
| Constants | SCREAMING_SNAKE_CASE | `const BATCH_SIZE: usize = 100;` |
|
|
| Types | PascalCase | `type MyResult<T> = Result<T, Error>;` |
|
|
|
|
### Imports
|
|
|
|
- **Absolute imports** for workspace crates: `use parsers::{detect_format, parse_metadata};`
|
|
- **Standard library** imports: `use std::path::Path;`
|
|
- **External crates**: `use sqlx::{postgres::PgPoolOptions, Row};`
|
|
- **Group by**: std → external → workspace → local (with blank lines between)
|
|
|
|
```rust
|
|
use std::collections::HashMap;
|
|
use std::path::Path;
|
|
|
|
use anyhow::Context;
|
|
use serde::{Deserialize, Serialize};
|
|
use sqlx::Row;
|
|
use uuid::Uuid;
|
|
|
|
use crate::error::ApiError;
|
|
use crate::AppState;
|
|
```
|
|
|
|
### Error Handling
|
|
|
|
- Use `anyhow` for application-level error handling with context
|
|
- Use `with_context()` for adding context to errors
|
|
- Return `Result<T, ApiError>` in API handlers
|
|
- Use `?` operator instead of manual match/unwrap where possible
|
|
|
|
```rust
|
|
// Good
|
|
fn process_book(path: &Path) -> anyhow::Result<Book> {
|
|
let file = std::fs::File::open(path)
|
|
.with_context(|| format!("cannot open file: {}", path.display()))?;
|
|
// ...
|
|
}
|
|
|
|
// Good - API error handling
|
|
async fn get_book(State(state): State<AppState>, Path(id): Path<Uuid>)
|
|
-> Result<Json<Book>, ApiError> {
|
|
let row = sqlx::query("SELECT * FROM books WHERE id = $1")
|
|
.bind(id)
|
|
.fetch_optional(&state.pool)
|
|
.await
|
|
.map_err(ApiError::internal)?;
|
|
// ...
|
|
}
|
|
```
|
|
|
|
### Database (sqlx)
|
|
|
|
- Use **raw SQL queries** with `sqlx::query()` and `sqlx::query_scalar()`
|
|
- Prefer **batch operations** using `UNNEST` for bulk inserts/updates
|
|
- Always use **parameterized queries** (`$1`, `$2`, etc.) - never string interpolation
|
|
- Follow existing patterns for transactions:
|
|
|
|
```rust
|
|
let mut tx = pool.begin().await?;
|
|
// ... queries ...
|
|
tx.commit().await?;
|
|
```
|
|
|
|
### Async/Tokio
|
|
|
|
- Use `tokio::spawn` for background tasks
|
|
- Use `spawn_blocking` for CPU-bound work (image processing, file I/O)
|
|
- Keep async handlers non-blocking
|
|
- Use `tokio::time::timeout` for operations with timeouts
|
|
|
|
```rust
|
|
let bytes = tokio::time::timeout(
|
|
Duration::from_secs(60),
|
|
tokio::task::spawn_blocking(move || {
|
|
render_page(&abs_path_clone, n)
|
|
}),
|
|
)
|
|
.await
|
|
.map_err(|_| ApiError::internal("timeout"))?
|
|
.map_err(ApiError::internal)?;
|
|
```
|
|
|
|
### Structs and Serialization
|
|
|
|
- Use `#[derive(Serialize, Deserialize, ToSchema)]` for API types
|
|
- Add `utoipa` schemas for OpenAPI documentation
|
|
- Use `Option<T>` for nullable fields
|
|
- Document public structs briefly
|
|
|
|
```rust
|
|
#[derive(Serialize, ToSchema)]
|
|
pub struct BookItem {
|
|
#[schema(value_type = String)]
|
|
pub id: Uuid,
|
|
pub title: String,
|
|
pub author: Option<String>,
|
|
// ...
|
|
}
|
|
```
|
|
|
|
### Performance Considerations
|
|
|
|
- Use **batch operations** for database inserts/updates (100 items recommended)
|
|
- Use **parallel iterators** (`rayon::par_iter()`) for CPU-intensive scans
|
|
- Implement **caching** for expensive operations (see `pages.rs` for disk/memory cache examples)
|
|
- Use **streaming** for large data where applicable
|
|
|
|
### Testing
|
|
|
|
- Currently there are no test files - consider adding unit tests for:
|
|
- Parser functions
|
|
- Thumbnail generation
|
|
- Configuration parsing
|
|
- Use `#[cfg(test)]` modules for integration tests
|
|
|
|
---
|
|
|
|
## 3. Project Structure
|
|
|
|
```
|
|
stripstream-librarian/
|
|
├── apps/
|
|
│ ├── api/ # REST API (axum) — port 7080
|
|
│ │ └── src/ # books.rs, pages.rs, thumbnails.rs, state.rs, auth.rs...
|
|
│ ├── indexer/ # Background indexing service — port 7081
|
|
│ │ └── src/ # worker.rs, scanner.rs, batch.rs, scheduler.rs, watcher.rs...
|
|
│ └── backoffice/ # Next.js admin UI — port 7082
|
|
├── crates/
|
|
│ ├── core/ # Shared config (env vars)
|
|
│ │ └── src/config.rs
|
|
│ └── parsers/ # Book parsing (CBZ, CBR, PDF)
|
|
├── infra/
|
|
│ └── migrations/ # SQL migrations (sqlx)
|
|
├── data/
|
|
│ └── thumbnails/ # Thumbnails générés par l'API
|
|
├── libraries/ # Book storage (mounted volume)
|
|
└── docker-compose.yml # À la racine (pas dans infra/)
|
|
```
|
|
|
|
### Key Files
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `apps/api/src/books.rs` | Book CRUD endpoints |
|
|
| `apps/api/src/pages.rs` | Page rendering & caching (LRU + disk) |
|
|
| `apps/api/src/thumbnails.rs` | Endpoints pour créer des jobs thumbnail (rebuild/regenerate) |
|
|
| `apps/api/src/state.rs` | AppState, Semaphore concurrent_renders |
|
|
| `apps/indexer/src/scanner.rs` | Phase 1 discovery : scan rapide sans I/O archive, skip dossiers inchangés |
|
|
| `apps/indexer/src/analyzer.rs` | Phase 2 analysis : `analyze_book` + génération thumbnails WebP |
|
|
| `apps/indexer/src/batch.rs` | Bulk DB ops via UNNEST |
|
|
| `apps/indexer/src/worker.rs` | Job loop, watcher, scheduler orchestration |
|
|
| `crates/parsers/src/lib.rs` | Format detection, metadata parsing |
|
|
| `crates/core/src/config.rs` | Configuration from environment |
|
|
| `infra/migrations/*.sql` | Database schema |
|
|
|
|
---
|
|
|
|
## 4. Common Patterns
|
|
|
|
### Configuration from Environment
|
|
|
|
```rust
|
|
// In crates/core/src/config.rs
|
|
impl IndexerConfig {
|
|
pub fn from_env() -> Result<Self> {
|
|
Ok(Self {
|
|
listen_addr: std::env::var("INDEXER_LISTEN_ADDR")
|
|
.unwrap_or_else(|_| "0.0.0.0:7081".to_string()),
|
|
database_url: std::env::var("DATABASE_URL")
|
|
.context("DATABASE_URL is required")?,
|
|
// ...
|
|
})
|
|
}
|
|
}
|
|
```
|
|
|
|
### Path Remapping
|
|
|
|
```rust
|
|
fn remap_libraries_path(path: &str) -> String {
|
|
if let Ok(root) = std::env::var("LIBRARIES_ROOT_PATH") {
|
|
if path.starts_with("/libraries/") {
|
|
return path.replacen("/libraries", &root, 1);
|
|
}
|
|
}
|
|
path.to_string()
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Important Notes
|
|
|
|
- **Workspace**: This is a Cargo workspace. Always specify the package when building specific apps.
|
|
- **Dependencies**: External crates are defined in workspace `Cargo.toml`, not individual `Cargo.toml`.
|
|
- **Database**: PostgreSQL is required. Run migrations before starting services.
|
|
- **External Tools**: 4 system tools required — `unrar` (CBR page count), `unar` (CBR extraction), `pdfinfo` (PDF page count), `pdftoppm` (PDF page render). Note: `unrar` and `unar` are distinct tools.
|
|
- **Thumbnails**: generated by the **indexer** service (phase 2, `analyzer.rs`). The API only creates jobs in DB — it does not generate thumbnails directly.
|
|
- **Sub-AGENTS.md**: module-specific guidelines in `apps/api/`, `apps/indexer/`, `apps/backoffice/`, `crates/parsers/`.
|