Домашний Netflix за вечер: Transmission + Jellyfin + Telegram-бот на Docker с поддержкой NAS

от автора

У меня дома стоит роутер 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.gitcd 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/TransmissionSMB_USER=adminSMB_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 / macOSbash 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               STATUStransmission       Upjellyfin           Uptransmission-bot   Up

Открываем:

Как это работает внутри — три нетривиальных момента

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/Downloadsmkdir -p /downloads/.incompletemkdir -p /downloads/watch# Патчим settings.jsonSETTINGS=/config/settings.jsonif [ -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|' $SETTINGSfi

В логах видно что скрипт выполнился:

[custom-init] Files found, executing[custom-init] 01-fix-settings.sh: executing...[custom-init] 01-fix-settings.sh: exited 0Connection 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. Не нужно открывать лишние порты и нет сетевых задержек между контейнерами.

Команды

Команда

Что делает

/list

Список всех торрентов с прогресс-барами и кнопками управления

/active

Только активные загрузки

/stats

Скорости, статистика за всё время

/help

Справка

Добавление торрентов

Три способа:

  1. Отправить .torrent файл боту прямо в чат

  2. Отправить magnet: ссылку текстом

  3. Кинуть .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 — первый запуск

  1. Открыть http://localhost:8096

  2. Пройти мастер настройки (язык, создать пользователя)

  3. Добавить медиатеку → тип “Фильмы” → путь /media/Downloads

  4. 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

Буду рад звёздочке ⭐ и вопросам в комментариях — особенно если столкнётесь с другими граблями на своём железе.

ссылка на оригинал статьи https://habr.com/ru/articles/1025302/