Metadata-Version: 2.4
Name: clonbuster
Version: 0.14.0
Summary: Detecta vídeos y fotos duplicados por hash perceptual y aparta las copias sobrantes a una cuarentena reversible.
License: AGPL-3.0-or-later
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: imagehash>=4.3
Requires-Dist: Pillow>=10
Requires-Dist: numpy>=1.26
Requires-Dist: tqdm>=4.66
Provides-Extra: web
Requires-Dist: fastapi>=0.110; extra == "web"
Requires-Dist: uvicorn>=0.29; extra == "web"
Requires-Dist: httpx>=0.27; extra == "web"
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: ruff>=0.5; extra == "dev"
Requires-Dist: httpx>=0.27; extra == "dev"
Requires-Dist: fastapi>=0.110; extra == "dev"
Requires-Dist: uvicorn>=0.29; extra == "dev"
Dynamic: license-file

<p align="center"><img src="assets/icono-256.png" width="128" alt="clonbuster"></p>

# clonbuster

Encuentra **vídeos y fotos** duplicados en una biblioteca, aunque las copias **no sean
idénticas** (otra resolución, otro bitrate, códec o formato, segundos recortados al principio o al
final, bandas negras, fotos giradas por EXIF), y aparta las sobrantes a una **cuarentena
reversible**. Nunca borra nada.

Funciona igual en Windows y en Ubuntu.

Forma parte de **Clipgeist**, tres apps independientes y self-hosted que trabajan juntas (ver
[más abajo](#clipgeist)).

> Especificación: [docs/SPEC.md](docs/SPEC.md) · Cambios: [CHANGELOG.md](CHANGELOG.md)

## Cómo funciona, en corto

1. **Escanear**: recorre la carpeta y anota de cada vídeo duración, resolución, bitrate y códec
   (con `ffprobe`).
2. **Huellas**: decodifica solo los fotogramas clave del vídeo (rápido; los cortos se muestrean
   cada 0,5 s), recorta las bandas negras, descarta los fotogramas casi uniformes y calcula un
   hash perceptual (pHash) de cada uno, con su instante.
3. **Comparar**: solo entre vídeos de duración parecida. Busca el mejor alineamiento entre las dos
   secuencias, así una copia con segundos de más al principio también se detecta. Para que sea un
   duplicado, tiene que coincidir **la mayor parte del vídeo**, no un tramo: dos episodios con la
   misma intro no se confunden.
4. **Fragmentos**: además, busca vídeos cortos que son un **trozo** de otro más largo (un clip
   recortado de un vídeo completo). Un índice por bandas del pHash evita comparar todos contra
   todos. El vídeo completo nunca se propone apartar; los fragmentos los decides tú.
5. **Informe**: un HTML con miniaturas de cada grupo, qué copia se conserva y cuáles se descartan,
   y también un CSV y un JSON.
**Fotos** (jpg, png, webp, heic, bmp, tiff, gif): cada una lleva dos hashes, pHash y dHash, y se
corrige la orientación EXIF. Dos fotos son duplicadas si **los dos** hashes coinciden y la
proporción es parecida. Exigir los dos evita confundir fotos parecidas pero distintas, como las de
una ráfaga.

6. **Aplicar**: mueve las descartadas (nunca los fragmentos) a una carpeta de cuarentena con la misma estructura de
   carpetas. `restaurar` lo deshace.

Ninguna fase anterior a `aplicar` modifica la biblioteca.

## Instalación

### Con Docker (recomendado)

```bash
git clone https://github.com/lucasfrattini/clonbuster.git && cd clonbuster
CARPETAS=/ruta/a/tus/videos docker compose up -d --build     # → http://<ip-del-servidor>:8095
```

También hay imagen publicada: `ghcr.io/lucasfrattini/clonbuster:latest` (amd64 y arm64; cada
versión `vX.Y.Z` con su etiqueta). Para instalar **las tres apps de Clipgeist juntas** en un
Debian/Ubuntu o como contenedor LXC de **Proxmox VE**, ver
[clipkeeper/deploy](https://github.com/lucasfrattini/clipkeeper/tree/main/deploy).

- `CARPETAS` es el **límite**: lo máximo que la web puede recorrer. Se monta en el contenedor en la
  misma ruta, así que las rutas que ves en la web son las reales.
- Para varias carpetas o discos (por ejemplo, un NFS montado en `/mnt`), usa un
  `docker-compose.override.yml` con más volúmenes y `--limites /ruta1,/mnt`. Si un disco se monta
  con automount después de arrancar, móntalo con `bind.propagation: rslave`.
- Datos (bases de datos, ubicaciones, bibliotecas y conexiones) en `./datos/`: es lo único que hay
  que respaldar.
- La web pide **usuario y contraseña** (se crean la primera vez que entras). Aun así, úsala solo en tu
  red local o detrás de una VPN.

### Después de instalar

1. Abre `http://<ip-del-servidor>:8095` y crea tu usuario.
2. Guarda una **ubicación**: la carpeta del servidor desde la que vas a elegir, dentro de `CARPETAS`.
3. Añade una **biblioteca**, marca vídeos o fotos y lanza el análisis.
4. Si usas las otras apps de Clipgeist, conéctalas: ver [Clipgeist](#clipgeist).

### Actualizar

`git pull && docker compose up -d --build` desde la carpeta de clonbuster. Con el instalador de las
tres apps, vuelve a ejecutarlo: conserva datos y ajustes.

### Sin Docker (Python)

Requisitos: **Python 3.11+** y **ffmpeg/ffprobe** en el PATH.

```bash
# ffmpeg
sudo apt install ffmpeg            # Ubuntu
winget install Gyan.FFmpeg         # Windows (o: choco install ffmpeg)

# clonbuster
git clone https://github.com/lucasfrattini/clonbuster.git
cd clonbuster
python -m venv .venv
.venv/bin/pip install .            # Windows: .venv\Scripts\pip install .
```

Si falta ffmpeg, clonbuster lo avisa al arrancar y explica cómo instalarlo.

## Interfaz web

```bash
pip install ".[web]"
clonbuster web                      # abre http://127.0.0.1:8095
clonbuster web --host 0.0.0.0 --limites /srv   # para la red local; solo deja acceder a /srv
```

Guardas tus **ubicaciones** (las carpetas del servidor desde las que eliges, siempre dentro de
`--limites`), agregas **bibliotecas** navegando o pegando la ruta, marcas vídeos y/o fotos y analizas. Ves cada
grupo con miniaturas (clic para reproducir el vídeo o ver la foto) y marcas qué borrar; viene
marcado todo menos la copia de mejor calidad. Los fragmentos tienen su propia pestaña y no vienen
marcados. **Mover a la papelera** se puede deshacer desde
la pestaña Papelera, donde también está **Eliminar definitivamente** (con una confirmación).

Las huellas se guardan también en un almacén común (`datos/huellas.db`): si registras una carpeta
y una subcarpeta suya como bibliotecas distintas, la segunda no vuelve a calcularlas.

**Ajustes → Automático**:

- **Análisis programado**: cada noche, a la hora elegida, analiza lo nuevo de todas tus
  bibliotecas, una detrás de otra (solo calcula las huellas de lo que no tenía).
- **Vaciado de la papelera**: borra solo lo que lleve más de 7, 14, 30 o 90 días. Desactivado por
  defecto.
- **Avisos**: al terminar un análisis (con cuántos grupos hay por revisar), si falla o al vaciar la
  papelera, un POST con un texto a la dirección que pongas. Sirve tal cual con
  [ntfy](https://ntfy.sh), Gotify o un webhook de Home Assistant; hay un botón de prueba.

### Copia de tus decisiones

En *Ajustes* puedes descargar y restaurar los grupos marcados como «no son duplicados», para no
revisarlos de nuevo si reinstalas.

## Clipgeist

clonbuster es una de tres apps que se conectan entre sí, al estilo de Sonarr, Radarr y Prowlarr:

| App | Qué hace | Puerto |
| --- | --- | --- |
| **clipscout** | Busca vídeos en sitios web | `3001` |
| **clonbuster** | Detecta duplicados y los aparta (esta app) | `8095` |
| **clipkeeper** | Catálogo, reproductor y orden del disco | `8096` |

**Conectarlas** — cada app tiene una pestaña **Ajustes**:

1. En la app B: **Ajustes → Clave de API → Copiar**.
2. En la app A: **Ajustes → Apps conectadas → + Añadir conexión**, eliges B, revisas la URL
   sugerida (`http://<ip-del-servidor>:<puerto>`), pegas la clave, **Probar** y **Guardar**.
3. Con «Conectar también en sentido contrario» marcado (por defecto), B queda conectada a A sin
   hacer nada más.

**O sin tocar nada**: con la misma `CLIPGEIST_CLAVE` en las tres y las URLs `CLIPGEIST_CLIPSCOUT_URL`,
`CLIPGEIST_CLIPKEEPER_URL` y `CLIPGEIST_CLONBUSTER_URL` en el entorno (por ejemplo en el `.env` de
Docker), cada app se conecta sola con las otras al arrancar.

Con clipkeeper conectado, clonbuster analiza al momento lo que clipkeeper encuentra nuevo, le
cuenta en la ficha de cada vídeo si tiene copias, y usa su catálogo para elegir qué copia conservar
cuando la calidad es equivalente.

La cabecera muestra un icono por cada app conectada para saltar de una a otra. El protocolo
(endpoints, cabecera `X-Api-Key`, formato de errores) está en [docs/CREW.md](docs/CREW.md); el estado
de Clipgeist se guarda en `datos/crew.json`.

## Uso desde la terminal

```bash
clonbuster todo "D:\Vídeos"                # escanea, compara y genera el informe
# revisa informe_clonbuster/informe.html
clonbuster aplicar --dry-run               # qué movería, sin tocar nada
clonbuster aplicar                         # mueve a cuarentena (pide confirmación)
clonbuster restaurar                       # deshace el último lote
```

Las fases también se pueden lanzar sueltas: `escanear <raiz>`, `huellas`, `comparar`, `informe`,
`aplicar`, `restaurar`. Todo se guarda en `clonbuster.db` (SQLite, en la carpeta desde la que
ejecutas). Si se interrumpe (Ctrl+C, apagado), al relanzar sigue donde iba. Si un vídeo cambia de
tamaño o de fecha se reprocesa, y los que desaparecen se marcan.

| Opción | Para qué |
| --- | --- |
| `--tipos videos` / `--tipos fotos` | buscar solo uno de los dos (por defecto, ambos) |
| `--db fichero.db` | una base de datos por biblioteca |
| `--workers N` | procesos en paralelo (por defecto, núcleos − 1) |
| `--config fichero.toml` | configuración; ver `clonbuster.toml.example` |
| `--salida carpeta` | dónde se escribe el informe |
| `aplicar --cuarentena carpeta` | por defecto `<raiz>/_clonbuster_cuarentena` |
| `aplicar --yes` | sin pedir confirmación |
| `restaurar --lote ID` | un lote concreto (el ID lo muestra `aplicar`) |
| `restaurar --desde-log ruta` | restaura con `movimientos.jsonl` aunque se haya perdido la DB |

La cuarentena está dentro de la propia biblioteca a propósito: mover dentro del mismo disco es
instantáneo (se renombra, no se copia). El escaneo la ignora.

### Qué copia se conserva

La de **mayor resolución**. Si empatan, la de **mayor bitrate** (solo vídeos), y si siguen
empatadas, la más **grande** en bytes. Con clipkeeper conectado, entre copias de **calidad
equivalente** (resolución ±10 %, duración ±2 s y al menos el 70 % del bitrate) se prefiere la
catalogada en clipkeeper y luego la que está en la carpeta de una persona; nunca una claramente
peor. El criterio está aislado en `src/clonbuster/conservar.py` para cambiarlo fácil.

## Umbrales

| Parámetro | Defecto | Qué hace | Si lo subes | Si lo bajas |
| --- | --- | --- | --- | --- |
| `modo_huellas` | auto | `keyframes` (rápido), `denso` (decodifica todo) o `auto` (keyframes y, si hay menos de `min_keyframes`, denso) | — | — |
| `intervalo` | 0,5 s | modo denso: separación entre fotogramas muestreados | más rápido | más preciso |
| `ventana` | 2,5 s | margen para emparejar fotogramas de dos copias, que no tienen sus fotogramas clave en los mismos instantes | tolera fotogramas clave más espaciados | más estricto |
| `tolerancia_segundos` / `tolerancia_porcentaje` | 5 s / 3 % | diferencia de duración admitida (se usa la mayor); también el desfase máximo que se busca | detecta recortes más grandes | menos comparaciones |
| `umbral_hamming` | 12 de 64 | distancia máxima para que dos fotogramas cuenten como iguales | tolera recompresiones peores, más riesgo de falsos positivos | más estricto |
| `umbral_coincidencia` | 0,80 | fracción del vídeo **más corto** que tiene que coincidir | menos falsos positivos | detecta copias más editadas |
| `tolerancia_misma_duracion` / `umbral_misma_duracion` | 0,5 s / 0,50 | si dos vídeos de 1 min o más duran lo mismo (±0,5 s), basta con que coincida la mitad: así se detectan las copias muy recomprimidas o en 360p | — | — |
| `umbral_hamming_fotos` | 6 de 64 | fotos: pHash **y** dHash deben quedar por debajo | agrupa fotos casi iguales (ráfagas) | solo copias muy exactas |
| `tolerancia_aspecto` | 0,05 | fotos: diferencia de proporción admitida | — | — |
| `umbral_varianza` | 12 | descarta fotogramas casi negros o planos | descarta más | usa más fotogramas |
| `fragmentos` | true | buscar vídeos que son un trozo de otro (`--sin-fragmentos` lo apaga) | — | — |
| `fragmento_min_segundos` | 20 s | los vídeos más cortos no se buscan como fragmento | menos avisos por intros y logos | encuentra clips más cortos |
| `umbral_fragmento` | 0,80 | fracción del corto que tiene que aparecer en el largo | menos falsos positivos | tolera clips editados |

Referencia: entre copias del mismo vídeo la distancia típica es 0–6; entre vídeos distintos, 25–35.

## Rendimiento

Solo se decodifican los fotogramas clave, así que lo que más cuesta es **leer** los ficheros: un
vídeo 1080p de 4 minutos tarda alrededor de medio segundo. Se hace una sola vez por vídeo y se
puede cancelar y retomar. Con
`hwaccel = "auto"` (o `cuda`, `vaapi`…) ffmpeg puede usar la GPU. Las huellas de un vídeo ocupan
unos pocos KB en la DB.

## Limitaciones conocidas

- Para que dos copias se emparejen, su duración tiene que caber en la tolerancia: una copia con
  minutos cortados no es un duplicado, sino un **fragmento** (se muestra aparte y no se aparta
  sola).
- No detecta copias **espejadas**, **giradas** de forma distinta, **aceleradas** ni con **marcos o
  logos grandes** superpuestos.
- Los vídeos que son casi todo negro o una imagen fija dan pocos fotogramas útiles: con menos de 5
  no se comparan.
- Por encima de 1 h (con los valores por defecto) el intervalo se duplica para no pasar de 7200
  fotogramas, y eso reduce la precisión del alineamiento.
- Fotos: un **recorte** o una foto con texto o marco añadido no se detecta como duplicado. Si dos
  copias tienen la misma resolución, gana la más pesada: una conversión a PNG de un JPEG puede
  ganarle al JPEG original.
- Formatos RAW (cr2, nef, arw…) no se incluyen.
- La agrupación es transitiva: si A≈B y B≈C, los tres quedan en el mismo grupo. Revisa el informe
  antes de aplicar.

## Desarrollo

```bash
uv venv && uv pip install -e ".[dev]"
ruff check src tests
pytest            # genera un set de vídeos sintéticos con ffmpeg (~20 s)
```

`docker compose run --rm clonbuster todo /biblioteca` (con `BIBLIOTECA=/ruta`) sirve si no
quieres instalar Python ni ffmpeg.

Historial de versiones en [CHANGELOG.md](CHANGELOG.md).

## Licencia

[GNU AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.html): libre para usar, alojar en tu propio
servidor y modificar, también con fines comerciales. Si distribuyes una versión modificada —o la
ofreces como servicio en red a otras personas— tienes que publicar su código con la misma licencia.
El texto completo está en [`LICENSE`](LICENSE).
