perf(api,indexer): optimiser pages, thumbnails, watcher et robustesse fd

- Pages: mode Original (zero-transcoding), ETag/304, cache index CBZ,
  préfetch next 2 pages, filtre Triangle par défaut
- Thumbnails: DCT scaling JPEG via jpeg-decoder (decode 7x plus rapide),
  img.thumbnail() pour resize, support format Original, fix JPEG RGBA8
- API fallback thumbnail: OutputFormat::Original + DCT scaling au lieu
  de WebP full-decode, retour (bytes, content_type) dynamique
- Watcher: remplacement notify par poll léger sans inotify/fd,
  skip poll quand job actif, snapshots en mémoire
- Jobs: mutex exclusif corrigé (tous statuts actifs, tous types exclusifs)
- Robustesse: suppression fs::canonicalize (problèmes fd Docker),
  list_folders avec erreurs explicites, has_children default true
- Backoffice: FormRow items-start pour alignement inputs avec helper text,
  labels settings clarifiés

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-14 23:07:42 +01:00
parent fe54f55f47
commit 6947af10fe
15 changed files with 711 additions and 395 deletions

View File

@@ -1,7 +1,8 @@
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use std::sync::{Mutex, OnceLock};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BookFormat {
@@ -527,6 +528,40 @@ pub fn extract_page(path: &Path, format: BookFormat, page_number: u32, pdf_rende
}
}
/// Cache of sorted image names per archive path. Avoids re-listing and sorting on every page request.
static CBZ_INDEX_CACHE: OnceLock<Mutex<HashMap<PathBuf, Vec<String>>>> = OnceLock::new();
fn cbz_index_cache() -> &'static Mutex<HashMap<PathBuf, Vec<String>>> {
CBZ_INDEX_CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
/// Get sorted image names from cache, or list + sort + cache them.
fn get_cbz_image_index(path: &Path, archive: &mut zip::ZipArchive<std::fs::File>) -> Vec<String> {
{
let cache = cbz_index_cache().lock().unwrap();
if let Some(names) = cache.get(path) {
return names.clone();
}
}
let mut image_names: Vec<String> = Vec::new();
for i in 0..archive.len() {
let entry = match archive.by_index(i) {
Ok(e) => e,
Err(_) => continue,
};
let name = entry.name().to_ascii_lowercase();
if is_image_name(&name) {
image_names.push(entry.name().to_string());
}
}
image_names.sort_by(|a, b| natord::compare(a, b));
{
let mut cache = cbz_index_cache().lock().unwrap();
cache.insert(path.to_path_buf(), image_names.clone());
}
image_names
}
fn extract_cbz_page(path: &Path, page_number: u32, allow_fallback: bool) -> Result<Vec<u8>> {
let file = std::fs::File::open(path)
.with_context(|| format!("cannot open cbz: {}", path.display()))?;
@@ -534,18 +569,7 @@ fn extract_cbz_page(path: &Path, page_number: u32, allow_fallback: bool) -> Resu
match zip::ZipArchive::new(file) {
Ok(mut archive) => {
let mut image_names: Vec<String> = Vec::new();
for i in 0..archive.len() {
let entry = match archive.by_index(i) {
Ok(e) => e,
Err(_) => continue,
};
let name = entry.name().to_ascii_lowercase();
if is_image_name(&name) {
image_names.push(entry.name().to_string());
}
}
image_names.sort_by(|a, b| natord::compare(a, b));
let image_names = get_cbz_image_index(path, &mut archive);
let selected = image_names
.get(index)