No description
  • Python 76.1%
  • HTML 10.8%
  • CSS 9.4%
  • JavaScript 3.5%
  • Dockerfile 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Victor Ruiz b71275ce6b
All checks were successful
Build and Deploy Telegramarr / build-and-deploy (push) Successful in 1m34s
Calibre: route uploads through public URL with Sablier wake handling
Calibre-Web is now scaled to zero by Sablier when idle, so the private
http://calibre:8083 hostname disappears while it sleeps and uploads fail
with '[Errno -2] Name or service not known'. Point the adapter at the
public https://calibre.celor.es router (which wakes the container) and
survive the wake window: retry transient 5xx and self-redirects that the
blocking Sablier strategy can emit while Traefik refreshes its backend
list, and default the client timeout to 180s so a cold start fits inside
a single request.
2026-10-05 11:20:48 +02:00
.forgejo/workflows chore(deploy): standardize cache/broker on valkey 9.1.2-alpine 2026-09-30 16:37:08 +02:00
.opencode feat(indexing): persist source index history 2026-08-21 11:19:15 +02:00
app Calibre: route uploads through public URL with Sablier wake handling 2026-10-05 11:20:48 +02:00
deploy chore(deploy): move the database to Postgres 18 (docker.celor.es/postgres:18-alpine) 2026-10-01 12:35:18 +02:00
tests Calibre: route uploads through public URL with Sablier wake handling 2026-10-05 11:20:48 +02:00
.dockerignore chore(docker): multi-stage build + static ffmpeg to shrink image (refs #12) 2026-08-23 21:39:09 +02:00
.env.example feat(indexing): persist source index history 2026-08-21 11:19:15 +02:00
.gitignore feat(ui): show first-run setup panel on login + docs (refs #3) 2026-08-23 22:37:39 +02:00
.sops.yaml ci: mobile deploy to /apps/forgejo via Forgejo Actions 2026-08-23 11:12:29 +02:00
AGENTS.md feat(ui): redesign library, sidebar, and design tokens 2026-08-19 19:32:28 +02:00
docker-compose.yml chore(deploy): move the database to Postgres 18 (docker.celor.es/postgres:18-alpine) 2026-10-01 12:35:18 +02:00
Dockerfile chore(docker): drop unused ffprobe binary (-124MB) (refs #12) 2026-08-23 22:10:02 +02:00
pyproject.toml feat: add role-based access control and OIDC/local authentication 2026-08-19 11:13:57 +02:00
README.md chore(deploy): move the database to Postgres 18 (docker.celor.es/postgres:18-alpine) 2026-10-01 12:35:18 +02:00
TODO.md feat(services): add public_base_url for external links 2026-08-20 14:51:25 +02:00
uv.lock feat: add role-based access control and OIDC/local authentication 2026-08-19 11:13:57 +02:00

Telegramarr

Telegramarr indexes files from Telegram chats, lets you select them in a web UI, downloads them through a worker queue, and uploads them to one or more configured destinations.

The application has two runtime processes:

  • Web: FastAPI UI, configuration pages, authentication, and job dispatch.
  • Worker: Dramatiq actors that index Telegram sources, create thumbnails, download files, and upload them to destinations.

PostgreSQL or SQLite stores application state. Redis transports background jobs and coordinates the workers. Downloaded files are staged under MEDIA_DIR.

Requirements

  • Python 3.12+ for a host-based installation.
  • Docker Engine and Docker Compose for the containerized installation.
  • A Telegram api_id and api_hash from my.telegram.org.
  • A Telegram account that can read the chats you configure as sources.
  • PostgreSQL and Redis for a multi-process or production deployment. SQLite is supported for a small local installation.

Ways To Run

Docker Compose

This is the recommended development and small-server setup. The repository's docker-compose.yml starts PostgreSQL, Redis, the web process, and one worker.

cp .env.example .env
# Edit .env: Telegram credentials, APP_ENCRYPTION_KEY, and authentication settings.

docker compose up -d db redis
docker compose up --build

Open http://localhost:8000. The web container runs with reload enabled; the worker runs separately and shares the database, Redis, Telegram configuration, and staged media directory.

Stop the application without deleting data:

docker compose down

The named volumes postgres_data, redis_data, and app_data persist across restarts. The default host media directory is ~/volumes/telegram-arr/media.

Compose Example

The following is the minimal shape of a complete deployment. Keep the database and Redis private; expose only the web service through a reverse proxy or local port.

services:
  db:
    image: postgres:18-alpine
    environment:
      POSTGRES_DB: media_ingestor
      POSTGRES_USER: media
      POSTGRES_PASSWORD: change-me
    volumes:
      - postgres_data:/var/lib/postgresql

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data

  web:
    build: .
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000
    env_file: .env
    environment:
      MEDIA_DIR: /media
    volumes:
      - app_data:/data
      - ./media:/media
    ports:
      - "8000:8000"
    depends_on:
      - db
      - redis

  worker:
    build: .
    command: dramatiq app.jobs.actors app.jobs.thumbnails --processes 1 --threads 10
    env_file: .env
    environment:
      MEDIA_DIR: /media
    volumes:
      - app_data:/data
      - ./media:/media
    depends_on:
      - db
      - redis

volumes:
  postgres_data:
  redis_data:
  app_data:

For this example, .env must point the containers at the Compose service names:

DATABASE_URL=postgresql+psycopg://media:change-me@db:5432/media_ingestor
REDIS_URL=redis://redis:6379/0
DATA_DIR=/data
MEDIA_DIR=/media

The checked-in Compose file also adds health checks, mounts the source tree for development, and uses the default host media path.

Local Python Processes

This mode is useful for development when the web process and worker should run directly on the host. SQLite can be used instead of PostgreSQL; Redis is still required for background jobs.

uv sync --extra dev
cp .env.example .env

Set these local values in .env:

DATABASE_URL=sqlite:///./media_ingestor.db
REDIS_URL=redis://localhost:6379/0
DATA_DIR=./data
MEDIA_DIR=./media

Start Redis separately. The repository Compose file keeps Redis private to the Compose network, so a host process needs a Redis instance with a published port:

docker run --name telegramarr-redis --detach --publish 6379:6379 redis:7-alpine

Remove that development container when finished with docker rm --force telegramarr-redis.

Run the two application processes in separate terminals:

uv run uvicorn app.main:app --reload --host 127.0.0.1 --port 8000
uv run dramatiq app.jobs.actors app.jobs.thumbnails --processes 1 --threads 10

Running only Uvicorn is sufficient for browsing and configuration, but queued indexing, downloads, thumbnails, and uploads will not be processed without a worker.

Separate Production Services

For a larger deployment, run the web and worker as separate containers or system services using the same image and environment. Point both at the same PostgreSQL database, Redis instance, APP_ENCRYPTION_KEY, and media volume. Run one or more worker replicas, but do not expose the worker directly to users.

The web process initializes the schema and starts the scheduler and dispatch watchdog. The worker consumes the Dramatiq queues. Both processes must use the same database and Redis configuration.

Configuration

Copy .env.example to .env. The most important settings are:

Variable Purpose
DATABASE_URL PostgreSQL or SQLite database URL.
REDIS_URL Redis connection used by Dramatiq.
DATA_DIR Persistent application data, including the optional legacy Telegram session.
MEDIA_DIR Local staging directory for downloaded files.
TELEGRAM_API_ID / TELEGRAM_API_HASH Telegram API credentials.
TELEGRAM_SESSION_STRING Optional memory-backed Telegram session.
APP_ENCRYPTION_KEY Fernet key for encrypted Telegram sessions and destination credentials.
INDEX_INTERVAL_SECONDS Periodic indexing interval; 0 disables it.
SESSION_SECRET Stable session-cookie signing key, especially important with multiple web replicas.

Generate an encryption key with:

uv run python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"

Keep .env, session strings, database files, and encryption keys private.

Telegram Authentication

Telegram authentication is separate from logging into the Telegramarr web application. Telegramarr uses a personal Telegram account through Telethon; it does not use a bot token.

There are two supported ways to provide the Telegram session:

  1. Browser setup: set APP_ENCRYPTION_KEY, open Telegram chats -> Authenticate Telegram, and complete the phone, code, and optional 2FA prompts. The resulting session is encrypted in the shared database so both web and worker processes can use it.

  2. Session string: create a memory-backed session interactively and put the result in .env:

    docker compose run --rm web python -c "from app.telegram.client import create_session_string; print(create_session_string())"
    

    Set the output as TELEGRAM_SESSION_STRING=.... Do not commit or share it; it grants access to the Telegram account.

For an existing legacy SQLite session, export it once with export_session_string() and then migrate to TELEGRAM_SESSION_STRING or browser setup. Runtime web and worker clients use memory-backed sessions and do not share a SQLite Telegram session file.

Domain Model

The application separates Telegram discovery, local staging, routing, and destination state.

Telegram chat
      |
      v
   Source --------------------+
      |                       |
      | indexes               | owns routing rules
      v                       v
TelegramMessage          RoutingRule -----> Service (destination)
      |                       |                  |
      | provenance            | matches          | receives uploads
      v                       v                  v
     Item -------------------------------> RemoteItem
      |
      +---- Asset(s) ---- local staged file / thumbnail

Source

A Source represents one Telegram chat, identified by a username or numeric chat ID. It stores the incremental message cursor, enabled state, and the media groups/extensions to ingest. A source can have many items and routing rules.

TelegramMessage

This is the raw indexed Telegram message record. It preserves message text, timestamps, file metadata, and Telegram-specific metadata for deduplication and provenance. It is not the object downloaded by the user.

Item

An Item is the library record created from an indexed file message. It has a title, content type, publication date, and processing status. The current indexer creates one item per Telegram message containing a file.

Asset

An Asset is a file belonging to an item. It stores the Telegram message ID, filename, MIME type, size, SHA-256, local staging path, thumbnail path, and media metadata. Downloading an item downloads its assets.

RoutingRule

A RoutingRule connects one source to one destination service. It can optionally match an extension, a media type, or both. Rules are evaluated independently, so one item can match multiple services. If several rules match the same service, that service is still uploaded only once.

Service

A Service is a configured destination. Supported adapter types are:

  • local_folder: hard-link or copy files to a local directory.
  • generic_http: send multipart uploads to a compatible /api/upload endpoint.
  • calibre: upload to Calibre-Web and preserve supported EPUB metadata.
  • audiobookshelf: upload to an Audiobookshelf library and run its follow-up enrichment workflow.

Connection details and service credentials are configured from the Services page. Set APP_ENCRYPTION_KEY before entering credentials; secret values are stored in encrypted database records.

RemoteItem

A RemoteItem records the relationship between an item and a service. It stores the remote ID, destination URL, upload time, and last verification time. The unique (item, service) relationship prevents duplicate destination records.

Jobs And Index Tasks

  • IndexTask records a source indexing run, including incremental/full mode, progress, imported count, errors, and timestamps.
  • Job records queued work such as downloads, uploads, retries, and destination-specific enrichment.
  • Redis transports work to Dramatiq; the database remains the durable source for status and progress.

Processing Flow

  1. An enabled source is indexed manually or by the periodic scheduler.
  2. Telegram file messages are filtered by the source's format groups and extension allowlists.
  3. New TelegramMessage, Item, and Asset records are stored. Existing Telegram messages are ignored on incremental runs.
  4. Thumbnail jobs are queued for assets where applicable.
  5. A user selects an item and requests a download. The worker downloads its assets into MEDIA_DIR/<item_id>/ and records progress and hashes.
  6. Matching routing rules determine the destination services.
  7. Upload jobs send the staged files to each matched service and persist a RemoteItem with the resulting URL or remote ID.

Periodic indexing runs every INDEX_INTERVAL_SECONDS seconds for enabled sources. Set it to 0 when indexing should be manual only.

First End-To-End Test

  1. Configure Telegram API credentials and authenticate the Telegram account.
  2. Log in as an administrator.
  3. Create a Service.
  4. Create a Source using a Telegram username such as @channelname.
  5. Select the source's media groups and optional extension allowlists.
  6. Create a Routing Rule connecting the source and service.
  7. Click Index now on the source.
  8. Open the Library and select Download for an indexed item.
  9. Watch the Queue page, then open the item to confirm its destination URL.

First-run setup

On a fresh install (empty user table) Telegramarr writes a .firstrun marker in DATA_DIR and the login page offers a Create the first account form. The account you create becomes an administrator and the marker is removed, disabling setup mode permanently.

  • To force the guided flow in a test or seeded environment, create an empty file DATA_DIR/.firstrun before starting the stack.
  • To skip it (e.g. you provision an admin via the CLI or an external seed), ensure the marker is absent; setup mode stays off and the existing account is used as-is.
  • No insecure default credential is ever created — you supply the password through the form, hashed with Argon2id.

Application Authentication

Every protected route is checked server-side. The two application roles are:

Role Access
admins Library, sources, Telegram setup, routing rules, services, queue, and user management.
users Library access, item fetching, and downloads of already-staged files.

OIDC Single Sign-On

OIDC uses Authlib with discovery, authorization-code flow, PKCE, and ID-token/userinfo validation. It works with providers such as Authentik, Keycloak, and Auth0.

Configure the provider in .env:

OIDC_ENABLED=true
OIDC_ISSUER=https://authentik.example.com/application/o/telegramarr/
OIDC_CLIENT_ID=telegramarr
OIDC_CLIENT_SECRET=replace-me
OIDC_REDIRECT_URI=https://telegramarr.example.com/auth/callback
OIDC_SCOPES=openid profile email
OIDC_ROLE_CLAIM=groups
OIDC_ADMIN_VALUES=admins
OIDC_USER_VALUES=users

Register the exact OIDC_REDIRECT_URI with the identity provider. On login, the configured role claim is compared with the admin and user value lists. If the database has no users, the first successful OIDC login is promoted to admins; later users receive the role derived from their claims. An administrator can pin a user's role in the Users page so future IdP claims do not change it.

Set OIDC_ENABLED=false when OIDC should not be offered.

Local Accounts

Local accounts use email and password. Passwords are stored as Argon2id hashes, not plaintext. Local login is enabled by default and can be disabled with LOCAL_LOGIN_ALLOWED=false.

Create the first administrator from the CLI:

docker compose run --rm web python -m app.cli local create-user \
  --email admin@example.com --admin

With a local Python installation:

uv run python -m app.cli local create-user \
  --email admin@example.com --admin

The CLI also supports set-password, set-role, set-enabled, and list-users. Failed logins are rate-limited and accounts are temporarily locked after repeated failures; configure the thresholds with LOCAL_MAX_FAILED_LOGINS, LOCAL_LOCKOUT_WINDOW_SECONDS, and LOCAL_LOCKOUT_DURATION_SECONDS.

Development Bypass

Set AUTH_DEV_BYPASS=true only for isolated local development. It skips all authentication and treats every request as a synthetic administrator. Never enable it on a shared or production deployment.

Security Notes

  • Use HTTPS for a deployed UI and set OIDC_REDIRECT_URI to the public HTTPS callback.
  • Set a stable SESSION_SECRET when multiple web instances share a load balancer.
  • Keep APP_ENCRYPTION_KEY, OIDC client secrets, Telegram session strings, and destination credentials out of source control.
  • Do not expose PostgreSQL or Redis to the public network.
  • Numeric Telegram chat IDs must be visible in the authenticated account's dialogs; usernames are generally easier to resolve.

Current Limitation

Grouped Telegram albums and multi-message book or audiobook releases are retained as message metadata but are not aggregated into one logical item. The current model therefore represents each Telegram file message as a separate item.

Project Layout

app/
  main.py         FastAPI application and web routes
  auth.py         OIDC and local-account authentication
  cli.py          Local user administration
  models.py       SQLAlchemy domain model
  telegram/       Telegram client, authentication, and indexer
  services/       Destination adapters
  jobs/           Dramatiq actors and scheduling/watchdogs
  templates/      Server-rendered HTML
  static/         CSS and browser JavaScript
docker-compose.yml
Dockerfile
.env.example