У меня дома стоит роутер 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:\, \\server\share) недоступны изнутри контейнера. 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/DownloadsJellyfin сам найдёт файлы, скачает постеры и описания с 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
Буду рад звёздочке ⭐ и вопросам в комментариях — особенно если столкнётесь с другими граблями на своём железе.
