Self-hosted media platform that streams your own files from a private Telegram channel to the browser, Android phone, Android TV, and desktop — powered by multi-bot parallel streaming with a two-tier (RAM + disk) cache.
![]() |
![]() |
![]() |
![]() |
aruvi/
├── backend/ # FastAPI backend (streaming engine, Telegram bot, API, subtitles)
├── frontend/ # React SPA (web player + file manager)
├── android/ # Native Android / Android-TV app (Kotlin + Jetpack Compose)
└── docs/ # Architecture, streaming, auth, data model, deployment guides
- Stream large media directly from your private Telegram channel to any device
- Multi-bot parallel chunk fetching with a RAM hot cache + disk cache (survives restarts)
- Internet subtitle search + download (OpenSubtitles.com + keyless providers)
- Native clients: Android app (phone + TV) and Windows/Linux desktop apps
- Google Drive integration, thumbnails, continue-watching, folder management
Aruvi ships no content. Each deployment is fully self-hosted against your own private Telegram channel:
- Create your storage — make a private Telegram channel, set its ID in
.env - Add your files — upload directly in Telegram or via the bot
- Stream anywhere — web player, Android phone/tablet, Android TV, or the Windows/Linux desktop apps
Your server never talks to anyone else's instance — files live only in channels you control.
The Android phone/tablet app can cast to Google Chromecast and Cast-enabled TVs
using the default media receiver (DefaultCastOptionsProvider). Casting is
Android-only — the web player and Android TV client stream directly and do not
expose a cast button.
The one rule that makes or breaks casting: the Chromecast device fetches the
video stream directly from whatever server URL the app is pointed at. The Cast
device is a separate piece of hardware on your network (or the internet) — it
cannot reach localhost, 127.0.0.1, or a LAN-only IP like 192.168.x.x.
If the app points at http://localhost:7680, discovery may still work but playback
will fail because the Cast device has no route to that address.
To cast from your own deployment, the server URL must be reachable by the Cast device. The easiest way is a public tunnel so the stream has a real, routable URL:
-
Cloudflare Tunnel (recommended, free, no open ports):
# Expose your backend (e.g. on :7680) at a public hostname cloudflared tunnel --url http://localhost:7680 # → https://movie.your-domain.tunnel.example.com
-
Or any reverse proxy / port forward that gives you a public
https://URL.
Then point the Android app at that public URL:
- Android: Settings → Server URL →
https://movie.your-domain.tunnel.example.com
The signed
AruviAPKs ship withhttps://movie.aaruvi.spaceas the default server, which is already publicly reachable — casting works out of the box there.
On Android 12+ the app needs Nearby devices (and, on Android 10/11, Location) permission to discover Cast devices over Wi-Fi. Grant it when prompted; discovery will not find devices without it.
cp .env.example .env # fill in your Telegram credentials first
docker compose up -d --build- Backend on
:7680(override viaSERVER_PORT) ./dataand./sessionbind-mounted for persistence
cd backend
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
cp ../.env.example .env # .env lives in backend/ for manual; repo root for Docker (both work via load_dotenv)
# fill in: TELEGRAM_API_ID/HASH, TELEGRAM_BOT_TOKEN, TELEGRAM_STORAGE_CHANNEL_ID, AUTH_USERS
mkdir -p data session # persisted: data/teleplay.db + data/.jwt_secret + session/*.session
python run.pyWeb UI served from backend/app/static (prebuilt SPA bundle included).
Windows / no
uvloop: usepython run_nouvloop.pyinstead (hardcodes7680). Requires Python 3.11+.
Copy .env.example → .env and fill in. Every cache/prefetch/concurrency knob is tunable — defaults match the live instance.
Newcomer? Get all IDs + session strings in one click:
— click it → wait 30s → run
python scripts/setup_helper.py→ follow prompts (phone → code). It prints yourTELEGRAM_STORAGE_CHANNEL_IDandGRAB_GROUP_USERNAMES+ session strings. No local install needed. Or use the web helper with input boxes: https://aaruvi.space/setup.html — fill API_ID/HASH + channel IDs → generate.envsnippet instantly (session strings still via Codespaces).
Easiest copy-paste (Codespaces or local)
pip install telethon
python scripts/setup_helper.py
# Choose 3) Both → enter API_ID/HASH (from https://my.telegram.org → API development tools)
# → phone (+91...) → code → 2FA if set
# Copies: GRAB_SESSION_STRINGS + all channel/group IDs (-100...) and usernamesFull step-by-step: docs/grabber.md:2.1.
This is the Telegram channel where your own files live. Aruvi only streams what you put there.
- In Telegram, create a new channel (private recommended, e.g.
My Aruvi Storage). - Add your bot (
@YourBotfrom BotFather) as admin — give itPost messagespermission. - Send any message to the channel, then forward it to
@userinfobot— it replies withID: -100.... Use that full-100...value.- Alt: forward to
@JsonDumpBotor callhttps://api.telegram.org/bot<token>/getUpdatesand readchat.id.
- Alt: forward to
- In
.envsetTELEGRAM_STORAGE_CHANNEL_ID=-100...(the-100prefix is required). - Restart the server (
python run.pyordocker compose restart). - Upload a test file to the channel — it should appear under
Home → Your Fileswithin seconds. If not, check logs forChannel access OK(grabber.logordocker logs).
Tip: for the movie-grabber, set GRAB_GROUP_USERNAMES=my_movie_group_1,my_group_2 (usernames only, no t.me//@) — requires a Telegram mobile account (phone number → user GRAB_SESSION_STRINGS, must join the groups). Full guide: docs/grabber.md.
cd frontend
npm install
npm run build # outputs to ../backend/app/staticcd android
cp local.properties.example local.properties
./gradlew assembleMobileDebug # phone/tablet
./gradlew assembleTvDebug # Android TV┌──────────────┐ ┌──────────────┐ ┌────────────────────┐
│ Your TV │────▶│ Aruvi API │────▶│ Telegram Channel │
│ / Browser │◀───│ (Port 7680) │◀───│ (Your Storage) │
└──────────────┘ └──────────────┘ └────────────────────┘
│
┌──────┴──────┐
│ Disk Cache │
│ /vcache/ │ ← persists across restarts
└─────────────┘
- 11 bots (1 main + 10 helpers) download chunks in parallel
- RAM cache: 300 MB/video (instant replay)
- Disk cache: 8 GB total / 2 GB/video (survives restarts)
- Refresh tokens rotate — replay attacks impossible
- Download tokens bind to file_id — prevents cross-user access
See docs/architecture.md for the full simple explanation.
| Topic | File |
|---|---|
| Architecture (5 min) | docs/architecture.md |
| API reference | docs/api.md |
| Streaming engine | docs/streaming.md |
| Auth & tokens | docs/auth.md |
| Database schema | docs/data-model.md |
| Grabber (add groups) | docs/grabber.md |
| Self-Hosting (Docker) | Self-Host Guide / docs/deployment.md |
| Testing | docs/testing.md |
| Deploy & runbook | docs/deployment.md |
| Agent cheat sheet | AGENTS.md |
| Variable | Default | Purpose |
|---|---|---|
TELEGRAM_API_ID |
— | Telegram app API ID |
TELEGRAM_API_HASH |
— | Telegram app API hash |
TELEGRAM_BOT_TOKEN |
— | Main bot token from BotFather |
TELEGRAM_HELPER_BOT_TOKENS |
empty | 10 helper bots for parallel fetching |
TELEGRAM_STORAGE_CHANNEL_ID |
— | Channel where media is stored (requires bot as admin, -100... via @userinfobot) |
AUTH_USERS |
— | Comma-separated Telegram user IDs allowed to log in (via @userinfobot; checked in bot.py) |
ADMIN_IDS |
— | Telegram IDs with /api/admin/* access |
DATABASE_URL |
SQLite (./data/teleplay.db) |
PostgreSQL supported (postgresql+asyncpg://...) — WAL+NORMAL, needs data/ writable |
JWT_SECRET |
auto-generated → data/.jwt_secret (0600) |
Set explicitly for multi-replica; else auto-generated once and persisted so restarts don't log everyone out |
SERVER_PORT |
7680 |
HTTP port (Docker; manual without .env defaults 24696) |
WEB_BASE_URL |
http://localhost:7680 |
Public base URL |
STREAM_RAM_PER_VIDEO_MB |
300 |
RAM hot cache per video |
STREAM_INFLIGHT_MB |
200 |
Backpressure cap per stream |
STREAM_PREFETCH_AHEAD_MB |
192 |
Prefetch ahead of playhead |
DISK_CACHE_DIR |
./data/vcache |
Disk cache location |
DISK_CACHE_TTL |
1800 |
Disk expiry (s) after last activity |
OPENSUBTITLES_API_KEY |
empty | Enables OpenSubtitles.com |
DEBUG_PASSWORD |
empty | For /diag/* endpoints |
GRAB_GROUP_USERNAMES |
empty | Comma-separated source group usernames (see docs/grabber.md) |
GRAB_BOT_USERNAMES |
empty | Bots per group, positional (empty = auto-detect) |
GRAB_SESSION_STRINGS |
empty | user sessions for parallel grabber fetching |
Full list in .env.example.
Built on the shoulders of TelePlay and MoviPlayer. Aruvi started as a fork and grew into its own thing with a rewritten backend, multi-user auth, desktop apps, and heavy reliability work. Huge thanks to those maintainers.
MIT



