У меня дома стоит роутер Keenetic с USB-диском на 2 ТБ. Долгое время он работал просто как сетевое хранилище — файлы лежат, Transmission на роутере крутится, всё вроде бы работает. Но дефолтный интерфейс Transmission выглядит как привет из 2009 года, управлять им с телефона неудобно, а посмотреть скачанное на телевизоре — вообще отдельный квест.
Однажды вечером решил это исправить. Итог — три Docker-контейнера, которые поднимаются одной командой, и теперь выглядит это так:
-
Открываю Telegram, кидаю боту
.torrentфайл -
Бот подтверждает: “✅ Добавлен!”
-
Через время пишет: “✅ Скачано! 📁 Название фильма · 💾 15 ГБ”
-
Открываю Jellyfin на телевизоре — фильм уже там, с постером и описанием на русском
Расскажу как это всё устроено.
Что получится в итоге
-
Transmission + Flood UI — торрент-клиент с современным интерфейсом вместо дефолтного
-
Jellyfin — медиасервер с постерами, описаниями, субтитрами. Работает на телевизоре, телефоне, в браузере
-
Telegram-бот — добавляй торренты и получай уведомления прямо в мессенджере
-
Watch-папка — кинул
.torrentфайл в папку на NAS, качается само -
Всё хранится на сетевом диске NAS (SMB/CIFS) и переживёт переустановку ОС
Стек и архитектура
┌─────────────────────────────────────┐
│ Docker на Windows │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Transmission │ │ Jellyfin │ │
│ │ + Flood UI │ │ :8096 │ │
│ │ :9091 │ └──────┬───────┘ │
│ └──────┬───────┘ │ │
│ │ ┌──────────┘ │
│ ┌──────┴─────┴──┐ │
│ │ Docker Volume │ │
│ │ (CIFS/SMB) │ │
│ └──────┬────────┘ │
│ ┌──────┴───────┐ │
│ │ Telegram Bot │ │
│ └──────────────┘ │
└─────────────┬───────────────────────┘
│ SMB
┌─────────────▼───────────────────────┐
│ NAS / Keenetic │
│ \192.168.1.ххTransmission │
│ ├── Downloads/ ← фильмы │
│ ├── .incomplete/ ← в процессе │
│ └── watch/ ← auto-add │
└─────────────────────────────────────┘
Ключевая идея — Docker volume типа CIFS монтирует сетевую шару напрямую внутрь контейнеров. Оба контейнера (Transmission и Jellyfin) работают с одними и теми же файлами на NAS: первый пишет, второй читает.
Требования
-
Docker Desktop (Windows / macOS) или Docker Engine (Linux)
-
NAS или роутер с USB-диском и SMB-шарой (Keenetic, Synology, QNAP и др.)
-
Telegram-аккаунт для бота
Нет NAS? В конце есть FAQ с вариантом на локальной папке.
Шаг 1. Клонируем репозиторий
git clone https://github.com/vervs3/mediabox.git
cd mediabox
Структура проекта:
mediabox/
├── docker-compose.yml
├── .env.example
├── bot/
│ ├── bot.py
│ ├── Dockerfile
│ └── requirements.txt
└── transmission/
├── setup-flood.sh
└── custom-cont-init.d/
└── 01-fix-settings.sh
Шаг 2. Настраиваем конфигурацию
cp .env.example .env
Редактируем .env:
# Часовой пояс
TZ=Europe/Moscow
# Сетевой диск (NAS, роутер Keenetic и т.д.)
SMB_HOST=//192.168.1.45/Transmission
SMB_USER=admin
SMB_PASSWORD=your_password
# Telegram-бот (получить у @BotFather)
BOT_TOKEN=123456789:AAxxxxx...
# Ваш Telegram ID (получить у @userinfobot)
ALLOWED_USER_ID=123456789
Шаг 3. Устанавливаем Flood UI
Здесь первые грабли. Я ожидал что образ linuxserver/transmission включает Flood прямо из коробки — раньше так и было. Но в свежих версиях сторонние UI убрали, и при запуске контейнер пишет:
Changes Required!
This image no longer bundles 3rd party Transmission UI packages.
We would advise you to use subfolders under /config to store your UI packages
Поэтому скачиваем Flood вручную:
# Linux / macOS
bash transmission/setup-flood.sh
Скрипт скачивает последний релиз flood-for-transmission и распаковывает в transmission/config/flood-ui/.
На Windows — скачайте архив вручную с GitHub releases и распакуйте в папку transmission/config/flood-ui/.
Шаг 4. Запускаем
docker compose up -d
При первом запуске Docker скачает образы (~700 МБ суммарно) и запустит все три сервиса.
Проверяем:
docker compose ps
NAME STATUS
transmission Up
jellyfin Up
transmission-bot Up
Открываем:
-
Transmission (Flood UI): http://localhost:9091
-
Jellyfin: http://localhost:8096
Как это работает внутри — три нетривиальных момента
1. Docker volume с CIFS вместо bind mount
Первое что я попробовал — пробросить сетевой диск Z: как обычный volume:
volumes:
- "Z:/:/downloads" # Не работает!
Не работает. Docker Desktop на Windows работает поверх WSL2, и смонтированные в Windows сетевые диски (Z:, \servershare) недоступны изнутри контейнера. WSL2 просто их не видит.
Решение — Docker volume с драйвером CIFS. Docker сам монтирует шару напрямую, минуя Windows:
volumes:
downloads:
driver: local
driver_opts:
type: cifs
device: ${SMB_HOST}
o: "username=${SMB_USER},password=${SMB_PASSWORD},vers=3.0,uid=1000,gid=1000,file_mode=0777,dir_mode=0777"
Оба контейнера — Transmission и Jellyfin — подключают этот volume и работают с одними файлами. Transmission пишет в /downloads/Downloads, Jellyfin читает из /media/Downloads (это один и тот же volume, просто под разными именами внутри контейнеров).
2. Патч настроек Transmission при каждом старте
Второй сюрприз — образ linuxserver/transmission при каждом запуске перезаписывает часть настроек дефолтными значениями. Пытаешься задать watch-dir через переменные окружения — не применяется. Правишь settings.json — при следующем старте снова сбрасывается.
Помогает механизм custom-cont-init.d: скрипты из этой папки выполняются после инициализации образа, но до старта transmission-daemon. Подкладываем туда патч:
#!/bin/sh
# Создаём нужные папки на шаре
mkdir -p /downloads/Downloads
mkdir -p /downloads/.incomplete
mkdir -p /downloads/watch
# Патчим settings.json
SETTINGS=/config/settings.json
if [ -f "$SETTINGS" ]; then
sed -i 's|"watch-dir": ".*"|"watch-dir": "/downloads/watch"|' $SETTINGS
sed -i 's|"watch-dir-enabled": false|"watch-dir-enabled": true|' $SETTINGS
sed -i 's|"download-dir": ".*"|"download-dir": "/downloads/Downloads"|' $SETTINGS
sed -i 's|"incomplete-dir": ".*"|"incomplete-dir": "/downloads/.incomplete"|' $SETTINGS
sed -i 's|"incomplete-dir-enabled": false|"incomplete-dir-enabled": true|' $SETTINGS
fi
В логах видно что скрипт выполнился:
[custom-init] Files found, executing
[custom-init] 01-fix-settings.sh: executing...
[custom-init] 01-fix-settings.sh: exited 0
Connection to localhost (127.0.0.1) 9091 port [tcp/*] succeeded!
3. Git Bash ломает пути в переменных окружения
Третий подводный камень специфичен для Windows + Git Bash. Когда передаёшь путь вида /flood-for-transmission/ через -e в docker run, Git Bash радостно превращает его в C:/Program Files/Git/flood-for-transmission/.
В логах это выглядит так:
ERR utils.cc:144 Couldn't read 'C:/Program Files/Git/flood-for-transmission//index.html'
Лечится переменной окружения перед командой:
MSYS_NO_PATHCONV=1 docker run ...
В docker-compose.yml эта проблема не возникает — там пути не проходят через интерпретатор Git Bash.
Telegram-бот
Бот работает в контейнере с network_mode: "service:transmission" — разделяет сетевой стек с Transmission и подключается к нему через localhost:9091. Не нужно открывать лишние порты и нет сетевых задержек между контейнерами.
Команды
|
Команда |
Что делает |
|---|---|
|
|
Список всех торрентов с прогресс-барами и кнопками управления |
|
|
Только активные загрузки |
|
|
Скорости, статистика за всё время |
|
|
Справка |
Добавление торрентов
Три способа:
-
Отправить
.torrentфайл боту прямо в чат -
Отправить
magnet:ссылку текстом -
Кинуть
.torrentв папкуwatchна NAS — Transmission подхватит сам
Прогресс-бар из юникода
Мелочь, но приятная — прогресс отображается прямо в тексте сообщения:
def progress_bar(pct, width=12):
filled = round(pct * width)
return "█" * filled + "░" * (width - filled)
# [████████░░░░] 67%
Уведомления о завершении без дублей
Чтобы бот не спамил уже завершёнными торрентами при рестарте, при старте инициализируем множество уже скачанных ID:
async def check_completed(app):
# При старте — молча запоминаем уже завершённые
torrents = await client.get_torrents(["id", "percentDone"])
completed_ids = {t["id"] for t in torrents if t["percentDone"] == 1.0}
while True:
await asyncio.sleep(CHECK_INTERVAL)
torrents = await client.get_torrents(["id", "name", "percentDone", "totalSize"])
for t in torrents:
if t["percentDone"] == 1.0 and t["id"] not in completed_ids:
completed_ids.add(t["id"])
# Только новые завершения — уведомляем
await notify_completed(app, t)
Когда торрент скачается, бот пришлёт:
✅ Скачано!
📁 Название.Фильма.2025.WEB-DL.1080p
💾 15.2 ГБ
[📋 Подробнее] [🗑 Удалить]
Jellyfin — первый запуск
-
Открыть http://localhost:8096
-
Пройти мастер настройки (язык, создать пользователя)
-
Добавить медиатеку → тип “Фильмы” → путь
/media/Downloads -
Jellyfin сам найдёт файлы, скачает постеры и описания с TMDB
Подключение с телевизора или телефона
Установите приложение Jellyfin (Android TV, Apple TV, Roku, Fire TV, iOS, Android) и введите:
http://192.168.x.x:8096
Где 192.168.x.x — локальный IP вашего компьютера (ipconfig на Windows, ip a на Linux).
FAQ
Q: Работает ли без NAS, просто на локальной папке?
Да. Замените volume в docker-compose.yml:
volumes:
downloads:
driver: local
driver_opts:
type: none
device: /absolute/path/to/folder
o: bind
Q: Как добавить VPN чтобы провайдер не видел торренты?
Добавьте сервис gluetun и переведите Transmission на его сеть:
transmission:
network_mode: "service:gluetun"
depends_on:
- gluetun
Q: Как обновить до новых версий образов?
docker compose pull && docker compose up -d
Q: Можно ли открыть доступ извне, не только в локальной сети?
Да, проще всего через Tailscale или Cloudflare Tunnel — без проброса портов на роутере.
Итог
Стек получился компактным — три контейнера, один docker-compose.yml, один .env файл с паролями. Поднимается с нуля за 5 минут, файлы живут на NAS и никуда не денутся при переустановке ОС.
Если у вас дома лежит роутер с USB-диском и вы до сих пор смотрите кино через проводник — попробуйте. Разница ощутимая.
Репозиторий: https://github.com/vervs3/mediabox
Буду рад звёздочке ⭐ и вопросам в комментариях — особенно если столкнётесь с другими граблями на своём железе.
Автор: vvs3
