Deterministic Telegram bot for storing, normalizing, and retrieving video clips through a fixed-layout Telegram UI and a structured S3-backed archive.
timeline-hub is built around a narrow but intentionally opinionated workflow:
- users send video clips in a private chat
- the bot briefly buffers incoming messages, reconstructs Telegram media albums, and presents a single action menu
- clips can then be normalized with
ffmpeg, stored into a structured archive, or fetched back through a deterministic selection flow
Stored clips are organized by year -> season -> universe -> sub-season -> scope. Each logical clip group lives under a single S3 prefix and is indexed by a manifest.json file. Fetching is driven by that manifest; storing uploads new clip objects first, then commits the updated manifest. If a later store stage fails after uploads begin, the manifest remains authoritative and manual cleanup may be required.
The runtime itself is small: aiogram handles Telegram updates, a compact service container exposes domain operations, and the infrastructure layer provides generic S3 and async task primitives. The distinctive part of the project is not framework complexity, but the strictness of the interaction model, storage semantics, and failure behavior.
- The Telegram UI is treated as a fixed spatial interface rather than a dynamic list. Inline-keyboard messages always render as exactly three text lines and three button rows.
- Menus use deterministic placement instead of left-to-right packing. Option grids follow a top-right-first snake layout, and unavailable options become inert dummy buttons instead of collapsing the layout.
- Store and Get/Pull flows deliberately share the same structural layouts. The UI can differ from the domain model at render time, while storage and parsing still use domain enums as the source of truth.
- Clip storage is manifest-backed rather than inferred from object listing alone. That gives strict ordering, subgroup discovery, validation, and clip-group-local deduplication by hash or existing clip id.
- Clip identity uses
video_hash, which hashes only the first encoded video stream after lossless elementary-stream extraction. MP4 container bytes, audio, subtitles, data streams, and container metadata are not part of clip identity, while the original uploaded bytes are still preserved in storage. - Background work is fail-fast. Detached task failures are logged, routed through a one-shot failure hook, and lead to superuser notification plus polling shutdown instead of silent degradation.
- Tests encode these choices directly. They verify keyboard slot placement, message padding, stale-menu handling, manifest shape, deduplication behavior, and async task semantics.
The project is split into small layers with narrow responsibilities.
appis the composition root. It parses the--devflag, loads immutable settings from.envandconfig.toml, configures logging, opens the Telegram bot and S3 client, installs allowlist middleware, injects the service container into the dispatcher, and wires handler or background-task failures to superuser notification and polling shutdown.handlersown Telegram-facing behavior.clips_store.pymanages buffered uploads, action selection, normalization, and storage menus.clips_retrieve.pydrives the Get/Pull flow from entry menu to batched clip delivery.clips_common.pyholds shared FSM state, menu parsing, text formatting, and fixed-layout keyboard primitives.servicesprovide the domain-facing operations used by handlers.ClipStoremaps logical clip groups to S3 objects and manifests,ChatMessageBufferbatches incoming messages per chat and reconstructs media groups, andServicesis an explicit container rather than implicit global state.infracontains reusable plumbing.S3Clientis a generic async wrapper over S3-compatible storage, andtasks.pyprovides a detached task supervisor plus a per-key debouncing scheduler.settingsloads secrets and allowlists from.env, loads non-secret runtime defaults fromconfig.toml, and automatically foldsSUPERUSER_IDSinto the main allowlist.domain.pycontains media-specific logic that does not belong in Telegram handlers. At the moment this is two-pass audio normalization viaffmpeg loudnorm, using temporary files because MP4 output must remain seekable.
At runtime, Telegram updates enter handlers, handlers translate them into domain-level operations, services execute those operations, and infrastructure provides the underlying storage and task primitives. The app layer stays responsible for wiring, lifecycle, and failure policy.
- Deterministic menu geometry. Messages with inline keyboards are rendered as either
padding, padding, contentorcontext, padding, prompt, and keyboards always occupy three rows. This removes layout shift and lets the user rely on position instead of rescanning the screen. - Snake-ordered option grids. The first two keyboard rows are filled from the top-right corner in a snake pattern. Newer or more relevant options naturally land in the easiest-to-reach positions without turning the menu into ad hoc per-step logic.
- UI as a projection of the domain.
Season,Universe,SubSeason, andScoperemain authoritative domain values, while the handler layer is free to reorder, group, or dummy-fill them for usability. That keeps storage and parsing stable while allowing a highly opinionated Telegram UI. - Manifest-backed clip groups. Each group under
<clip namespace>/<universe>-<year>-<season>/stores clip objects plus amanifest.jsonindex. The manifest is validated on load, defines subgroup membership and order, and acts as the commit boundary for store operations. - Encoded-stream clip identity. Clip deduplication is based on the first encoded video stream after lossless elementary-stream extraction. This is not whole-container hashing and not perceptual equivalence: the same visuals encoded differently, including across H.264 and HEVC, are not expected to dedupe.
- Buffered ingestion before user choice. Incoming private-chat messages are collected per chat and released after a short debounce. The bot can then present one menu for a burst of forwarded clips and preserve Telegram media-group structure when storing or resending.
- Fail-fast async orchestration. Detached tasks are supervised centrally. Debounced jobs can be rescheduled by key, but once real work starts it is shielded from cancellation, and unexpected failures trigger a clear shutdown path.
src/ timeline_hub/ app.py settings.py domain.py handlers/ core.py clips_common.py clips_retrieve.py clips_store.py router.py services/ clip_store.py message_buffer.py container.py infra/ s3.py tasks.py tests/ test_handlers.py test_clip_store.py test_s3.py test_tasks.py test_app.py test_chat_message_buffer.py
Python 3.14 is required.
uv sync --group dev
Create .env from .env.example. The bot operates only in private chats and requires an allowlist configuration.
The tracked config.toml already lives in the repository root and contains the non-secret runtime tuning. Production values live in the normal top-level sections, and optional development overrides can live under [dev.*] tables in the same file. Keep .env as the user-created local file for secrets.
Example:
[interface]
# Delay before the bot answers after a burst of forwarded messages
forward_batch_timeout_ms = 250
# Width used for the fixed padding line in menu messages
message_width = 80
[telegram]
# Maximum media-group payload size observed to be accepted by Telegram, in MiB
media_group_max_size = 47
[stores]
# Lowest year shown when choosing clip and track destinations
min_year = 2022
[clips]
# S3 namespace for clip manifests and media
namespace = "clips"
# Audio normalization target in LUFS
normalization_loudness = -14
# Audio bitrate for normalized output
normalization_bitrate = 128
[tracks]
# S3 namespace for track media, manifests, and presets
namespace = "tracks"
# Maximum clip duration for track variants, in minutes
variant_max_duration_minutes = 30
# Slowest allowed variant speed
slowest_variant_speed = 0.5
[dev.interface]
message_width = 72
[dev.telegram]
media_group_max_size = 10
[dev.clips]
namespace = "clips-dev"
[dev.tracks]
namespace = "tracks-dev"Required settings:
- BOT_TOKEN
- SUPERUSER_IDS
- S3__ENDPOINT_URL
- S3__REGION
- S3__BUCKET
- S3__ACCESS_KEY_ID
- S3__SECRET_ACCESS_KEY
Optional settings used by the code include:
- USER_IDS for additional allowlisted users beyond
SUPERUSER_IDS
ffmpeg must also be available on PATH; it is used for audio normalization and clip processing.
Run the bot through the package entry point:
uv run timeline-hub
Pass --dev to load development overrides from the same config.toml file:
uv run timeline-hub --dev
Local checks cover the same quality gates as CI, and pre-commit is the main gate before changes are committed.
uv run pytest uv run ruff check --fix uv run ruff format uv run pyright uv run pre-commit run --all-files
In this repository, pre-commit runs EOF/TOML checks, Ruff linting and formatting, and Pyright.
GitHub Actions runs on Python 3.14, installs dependencies with uv sync --group dev, and executes:
uv run ruff checkuv run pyrightuv run pytest
These checks run in separate jobs so lint, typing, and test failures stay easy to identify while remaining aligned with the local development workflow.
- Prefer deterministic behavior over compact or adaptive UI.
- Keep abstractions small and explicit; most modules have one job.
- Separate Telegram presentation from domain and storage semantics.
- Treat object storage as infrastructure and the clip archive as a domain service.
- Fail visibly when background work breaks instead of masking the problem.
- Encode invariants in tests, especially for layout and ordering rules.
- Clip flows are limited to private chats and gated by an allowlist middleware.
- FSM state uses in-memory storage, so conversational state is local to the running process.
ClipStoreassumes sequential writes per logical clip group; concurrent writers to the same group are not supported.- Fetching is batched, but ordering is always driven by manifest order, not by object listing order.
The code is set up to grow by composition rather than framework-building. New Telegram flows can reuse the shared menu primitives and FSM helpers in clips_common.py, while new storage-oriented behavior can build on the existing ClipStore and S3Client boundary without leaking Telegram concerns into infrastructure code.