post

Una carpeta que se procesa sola

30 de agosto de 2026 · homelab · bash, systemd, Calibre

Problema

Los libros que compro o descargo llegan en PDF y en EPUB, y el Kindle no se lleva bien con la mitad. El ritual era siempre el mismo: copiar el archivo al PC, abrir Calibre de escritorio, convertir, añadir a la biblioteca, mover el original a algún sitio y olvidarme de dónde lo dejé.

Quería quitar el ritual entero: dejar el archivo en una carpeta del NAS desde cualquier máquina de casa y que en unos minutos estuviera en la biblioteca, convertido si hacía falta, sin tocar nada más.

Solución

Tres piezas, ninguna nueva:

El script no vigila la carpeta ni escucha eventos del sistema de archivos. Se despierta, recorre lo que haya, y se va. Es más tonto y se rompe menos.

incoming/          ← aquí se deja el archivo
  processed/       ← el original, después de entrar bien
  unsupported/     ← lo que no es PDF ni EPUB
  error/           ← lo que falló al convertir o al indexar
ebooks/            ← la biblioteca (metadata.db vive aquí)

Lo que importa de esas carpetas: un archivo nunca se queda en incoming en un estado ambiguo. O entró, o no es del tipo correcto, o falló, y en los tres casos está en un sitio distinto.

Ejecución

El contenedor

LXC privilegiado (unprivileged: 0) con nesting=1, 2 vCPU y 2 GB de RAM. Privilegiado porque con NFS de por medio la gestión de permisos se convierte en su propio proyecto, y esto corre en la red de casa.

Los montajes van en /etc/pve/lxc/103.conf:

mp0: /mnt/pve/omv-books/books/incoming,mp=/mnt/incoming
mp1: /mnt/pve/omv-books/books/ebooks,mp=/mnt/ebooks

Calibre se instala con el binario oficial, no con el paquete de la distro:

wget -O- https://download.calibre-ebook.com/linux-installer.sh | sudo bash

El script

Vive en /usr/local/bin/calibre-process.sh. La conversión sale a un directorio temporal, para que lo que se archiva en processed sea exactamente lo que llegó y no una versión modificada.

#!/bin/bash

INCOMING="/mnt/incoming"
PROCESSED="/mnt/incoming/processed"
UNSUPPORTED="/mnt/incoming/unsupported"
ERROR_FOLDER="/mnt/incoming/error"
LIBRARY="/mnt/ebooks"
TEMP_DIR="/tmp/ebook_conversion_$$"

mkdir -p "$TEMP_DIR"

trap 'rm -rf "$TEMP_DIR"' EXIT

for FILE in "$INCOMING"/*; do
        [ -e "$FILE" ] || continue # no files
        [ -d "$FILE" ] && continue # skip directories

        EXT="${FILE##*.}"
        BASENAME=$(basename "$FILE")
        LOWER=$(echo "$EXT" | tr '[:upper:]' '[:lower:]')

        case "$LOWER" in
                pdf)
                        echo "Converting PDF -> EPUB: $BASENAME"
                        TEMP_OUT="$TEMP_DIR/${BASENAME%.pdf}.epub"

                        if ebook-convert "$FILE" "$TEMP_OUT" 2>/dev/null; then
                                if calibredb add --with-library "$LIBRARY" "$TEMP_OUT" 2>/dev/null; then
                                        mv "$FILE" "$PROCESSED/$BASENAME"
                                        mv "$TEMP_OUT" "$LIBRARY/${BASENAME%.pdf}.epub"
                                        echo "SUCCESS: $BASENAME processed"
                                else
                                        echo "ERROR: calibredb failed for $BASENAME"
                                        mv "$FILE" "$ERROR_FOLDER/$BASENAME"
                                        mv "$TEMP_OUT" "$ERROR_FOLDER/${BASENAME%.pdf}.epub" 2>/dev/null
                                fi
                        else
                                echo "ERROR: ebook-convert failed for $BASENAME"
                                mv "$FILE" "$ERROR_FOLDER/$BASENAME"
                        fi
                        ;;
                epub)
                        echo "Adding EPUB: $BASENAME"
                        if calibredb add --with-library "$LIBRARY" "$FILE"; then
                                mv "$FILE" "$PROCESSED/$BASENAME"
                                echo "SUCCESS: $BASENAME indexed"
                        else
                                echo "ERROR: calibredb failed for $BASENAME"
                                mv "$FILE" "$ERROR_FOLDER/$BASENAME"
                        fi
                        ;;

                *)
                        echo "Skipping unsupported file: $BASENAME"
                        mv "$FILE" "$UNSUPPORTED/$BASENAME" 2>/dev/null || echo "Error: failed to move $BASENAME"
                        ;;
        esac
done

Dos detalles que sí fueron decisiones. El trap ... EXIT borra el temporal pase lo que pase, incluso si el script muere a mitad. Y el EPUB no se convierte: se indexa tal cual, porque convertir EPUB a EPUB solo sirve para estropear el formato.

El disparador

Un servicio oneshot más un timer, en vez de cron. La ventaja práctica es que la salida del script queda en journalctl sin montar nada más.

# /etc/systemd/system/process-books.service
[Unit]
Description=Process incoming books for Calibre

[Service]
Type=oneshot
ExecStart=/usr/local/bin/calibre-process.sh
# /etc/systemd/system/process-books.timer
[Unit]
Description=Run book processing script every 5 minutes

[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
Unit=process-books.service

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now process-books.timer
journalctl -u process-books -f

Cómo ha funcionado

Lleva meses corriendo y hace lo que tenía que hacer: dejo el archivo, me voy, y el libro acaba en el Kindle. El día a día son cero intervenciones.

Lo que sí dio guerra al montarlo:

Lo que todavía no maneja, escrito aquí para acordarme: un archivo que se está copiando cuando el timer entra se procesa a medias y acaba en error; no hay flock, así que dos ejecuciones pueden solaparse si un lote grande dura más de cinco minutos; y no hay comprobación de duplicados, el mismo libro dos veces entra dos veces.