{"id":31165339,"url":"https://github.com/zxoir/cleanrr","last_synced_at":"2025-12-30T21:22:29.429Z","repository":{"id":311436576,"uuid":"1033424525","full_name":"Zxoir/cleanrr","owner":"Zxoir","description":"Automate media cleanup for Plex using WhatsApp, integrated with Overseerr, Radarr, and Sonarr.","archived":false,"fork":false,"pushed_at":"2025-08-24T13:48:29.000Z","size":1461,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-08-24T18:07:57.504Z","etag":null,"topics":["bullmq","docker","nodejs","overseerr","radarr","redis","sonarr","typescript","webhook","whatsapp"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Zxoir.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-08-06T19:47:02.000Z","updated_at":"2025-08-24T13:40:10.000Z","dependencies_parsed_at":"2025-08-24T18:08:08.671Z","dependency_job_id":"5e5652af-f38d-4afd-83e5-f9992f632333","html_url":"https://github.com/Zxoir/cleanrr","commit_stats":null,"previous_names":["zxoir/cleanrr"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/Zxoir/cleanrr","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Zxoir%2Fcleanrr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Zxoir%2Fcleanrr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Zxoir%2Fcleanrr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Zxoir%2Fcleanrr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Zxoir","download_url":"https://codeload.github.com/Zxoir/cleanrr/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Zxoir%2Fcleanrr/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":275902538,"owners_count":25549249,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-19T02:00:09.700Z","response_time":108,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bullmq","docker","nodejs","overseerr","radarr","redis","sonarr","typescript","webhook","whatsapp"],"created_at":"2025-09-19T08:10:07.244Z","updated_at":"2025-09-19T08:10:09.733Z","avatar_url":"https://github.com/Zxoir.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![CI](https://img.shields.io/github/actions/workflow/status/zxoir/cleanrr/ci.yml?branch=main)](https://github.com/zxoir/cleanrr/actions)\n[![Release](https://img.shields.io/github/v/release/zxoir/cleanrr)](https://github.com/zxoir/cleanrr/releases)\n[![License](https://img.shields.io/github/license/zxoir/cleanrr)](LICENSE)\n[![Stars](https://img.shields.io/github/stars/zxoir/cleanrr?style=social)](https://github.com/zxoir/cleanrr/stargazers)\n[![Node](https://img.shields.io/badge/node-%3E%3D20-339933?logo=node.js\u0026logoColor=white)](https://nodejs.org/)\n[![Architectures](https://img.shields.io/badge/arch-amd64%20%7C%20arm64-2ea44f)](#)\n[![Docker Pulls](https://img.shields.io/docker/pulls/zxoir/cleanrr?logo=docker)](https://hub.docker.com/r/zxoir/cleanrr)\n[![Image Size](https://img.shields.io/docker/image-size/zxoir/cleanrr/latest?logo=docker)](https://hub.docker.com/r/zxoir/cleanrr/tags)\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/zxoir/cleanrr\"\u003e\n    \u003cimg src=\"./assets/cleanrr.png\" alt=\"Cleanrr logo\" width=\"192\" height=\"192\"\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n# Cleanrr Bot – WhatsApp reminders for Overseerr (Docker-ready)\n\nA lightweight, self‑hosted WhatsApp bot that listens to Overseerr webhooks and, after a delay, asks the requester “Are you done watching?”. Optionally cleans up via Radarr/Sonarr when the user confirms. Built with Node.js + TypeScript and packaged for Docker.\n\n## Key features\n\n- **Webhook‑driven**: reacts to Overseerr events.\n- **WhatsApp DM**: sends a reminder and understands “yes/no” replies.\n- **Optional cleanup**: on “yes”, delete from Radarr/Sonarr (if configured).\n- **Durable scheduling**: BullMQ + Redis; idempotent jobs with backoff.\n- **Simple storage**: SQLite (file‑based), no external DB required.\n- **Production packaging**: multi‑stage Dockerfile, non‑root runtime, `/health` endpoint.\n\n## Tech stack\n\n- **Runtime**: Node 20, TypeScript (ESM/NodeNext)\n- **Web**: Express\n- **Bot**: Baileys (WhatsApp)\n- **Queue**: BullMQ + Redis\n- **Storage**: SQLite\n- **HTTP**: Axios\n- **Config/Validation**: dotenv + Zod\n- **Logging**: Pino (structured logs)\n\n## Requirements\n\n- Docker (recommended for users) or Node.js 20+ (for local dev)\n- Redis (single instance is enough)\n- Overseerr (and optionally Radarr/Sonarr API endpoints)\n\n## Quick start (Docker)\n\n1. Create `.env` (see **Environment** below).\n2. Start Redis (compose or your own).\n3. Run the container:\n\n```bash\ndocker run -d --name cleanrr   -p 3000:3000   --env-file .env   -v $(pwd)/session:/app/session   -v $(pwd)/data:/app/data   ghcr.io/\u003cyou\u003e/\u003crepo\u003e:latest\n```\n\n\u003e The container runs as a non‑root user and persists the WhatsApp session and SQLite DB under the mounted `session/` and `data/` directories.\n\n## Docker Compose example\n\n```yaml\nservices:\n  app:\n    image: docker.io/zxoir/cleanrr:latest\n    init: true\n    environment:\n      OVERSEERR_API_URL: ${OVERSEERR_API_URL}\n      OVERSEERR_API_KEY: ${OVERSEERR_API_KEY}\n      OVERSEERR_WEBHOOK_SECRET: ${OVERSEERR_WEBHOOK_SECRET-}\n      PORT: ${PORT-3000}\n      RADARR_API_URL: ${RADARR_API_URL-}\n      RADARR_API_KEY: ${RADARR_API_KEY-}\n      SONARR_API_URL: ${SONARR_API_URL-}\n      SONARR_API_KEY: ${SONARR_API_KEY-}\n      REDIS_URL: ${REDIS_URL-redis://redis:6379}\n      DB_PATH: /app/data/app.sqlite\n      WHATSAPP_SESSION_PATH: /app/session\n      MOVIE_DELAY_DAYS: ${MOVIE_DELAY_DAYS-0}\n      SHOW_DELAY_DAYS: ${SHOW_DELAY_DAYS-0}\n      RETRY_INTERVAL_HOURS: ${RETRY_INTERVAL_HOURS-24}\n      LOG_LEVEL: ${LOG_LEVEL-info}\n    ports:\n      - \"3000:3000\"\n    depends_on:\n      - redis\n    volumes:\n      - ./data:/app/data\n      - ./session:/app/session\n    healthcheck:\n      test:\n        [\n          \"CMD\",\n          \"node\",\n          \"-e\",\n          \"fetch('http://localhost:3000/health').then(r=\u003eprocess.exit(r.ok?0:1)).catch(()=\u003eprocess.exit(1))\"\n        ]\n      interval: 30s\n      timeout: 5s\n      retries: 3\n      start_period: 10s\n\n  redis:\n    image: redis:7-alpine\n    restart: unless-stopped\n    command: [\"redis-server\", \"--save\", \"60\", \"1\", \"--loglevel\", \"warning\"]\n    volumes:\n      - redis-data:/data\n\nvolumes:\n  redis-data:\n```\n\n## Environment\n\nCreate `.env` (or pass variables in your orchestrator). Values shown here are examples; adjust to your setup.\n\n| Variable                   | Type   | Required    | Default                | Description                                                                 |\n| -------------------------- | ------ | ----------- | ---------------------- | --------------------------------------------------------------------------- |\n| `OVERSEERR_API_URL`        | URL    | yes         | –                      | Overseerr API base, e.g. `https://overseerr.local/api/v1`                   |\n| `OVERSEERR_API_KEY`        | string | yes         | –                      | Overseerr API key                                                           |\n| `OVERSEERR_WEBHOOK_SECRET` | string | recommended | –                      | Shared secret; if set, webhook must include header `x-request-id: \u003csecret\u003e` |\n| `RADARR_API_URL`           | URL    | no          | –                      | Radarr API base (optional)                                                  |\n| `RADARR_API_KEY`           | string | no          | –                      | Radarr API key (optional)                                                   |\n| `SONARR_API_URL`           | URL    | no          | –                      | Sonarr API base (optional)                                                  |\n| `SONARR_API_KEY`           | string | no          | –                      | Sonarr API key (optional)                                                   |\n| `REDIS_URL`                | URL    | yes         | `redis://redis:6379`   | Redis connection string                                                     |\n| `DB_PATH`                  | path   | no          | `/app/data/app.sqlite` | SQLite file path inside container                                           |\n| `WHATSAPP_SESSION_PATH`    | path   | no          | `/app/session`         | Baileys session folder (bind‑mount for persistence)                         |\n| `MOVIE_DELAY_DAYS`         | number | no          | `0`                    | Days to wait before asking for movies (0 = immediate)                       |\n| `SHOW_DELAY_DAYS`          | number | no          | `0`                    | Days to wait before asking for series (0 = immediate)                       |\n| `RETRY_INTERVAL_HOURS`     | number | no          | `24`                   | Hours to wait before asking again after a “no”                              |\n| `PORT`                     | number | no          | `3000`                 | HTTP port inside container                                                  |\n| `LOG_LEVEL`                | enum   | no          | `info`                 | Pino log level: fatal/error/warn/info/debug/trace                           |\n\n## Overseerr webhook setup\n\n1. In Overseerr → **Settings → Notifications → Webhooks**, create a new webhook:\n   - **URL**: `http://\u003cYOUR_HOST\u003e:3000/overseerr`\n   - **Header (recommended)**: `x-request-id: \u003cOVERSEERR_WEBHOOK_SECRET\u003e` (must match your `.env`)\n2. Enable **Request Automatically Approved** and **Request Approved** notifications for the webhook (required).\n3. Save.\n\n## Linking WhatsApp \u0026 trying it\n\n1. Start the container and view logs; scan the QR when prompted (first run).\n2. Send the bot a DM: `!verify \u003cyour-overseerr-email\u003e` (this opts you in).\n3. Make a request in Overseerr. With delays set to `0`, the bot will message immediately.\n4. Reply `yes` to delete via Radarr/Sonarr (if configured), or `no` to be reminded later.\n5. Other commands:\n   - `!list` — show tracked titles/status\n   - `!test` — schedule a 1s test reminder\n   - (If implemented) `!delete \u003ctitle\u003e` — delete a specific title\n   - (If implemented) `!stop` / `!delete-me` — opt‑out and/or erase your data\n\n## Health \u0026 logs\n\n- **Health check**: `GET /health` returns `{ \"status\": \"ok\" }` when the app is ready.\n- **Logs**: Pino structured logs (JSON). In development, you can pipe through `pino-pretty` for readability.\n\n## Upgrading\n\n- Pull the latest image or update to a tagged semantic version (e.g., `:1.1.0`).\n- The database and session are persisted via volumes; upgrades are safe as long as you keep those mounts in place.\n- Breaking changes will be noted in release notes.\n\n## Security \u0026 privacy\n\n- Uses an **unofficial WhatsApp library (Baileys)**. Use at your own risk. Your WhatsApp number may be limited/banned if you violate WhatsApp policies. Only message users who **opt in** (they must DM the bot with `!verify \u003cemail\u003e`).\n- Not affiliated with or endorsed by **Meta/WhatsApp**, **Overseerr**, **Radarr**, or **Sonarr**.\n- Minimal data stored: WhatsApp JID ↔ email and request metadata. Avoid sharing your `.env`, logs with PII, or volumes publicly.\n- If you need to reset the WA session, stop the container and remove the `session/` volume; you’ll scan a fresh QR next start.\n\n## Development (optional)\n\n```bash\n# Node 20+\nnpm ci\nnpm run dev          # starts server in watch mode\nnpm run typecheck\nnpm run lint\nnpm run build\n```\n\n## License\n\nMIT License — see `LICENSE`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzxoir%2Fcleanrr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzxoir%2Fcleanrr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzxoir%2Fcleanrr/lists"}