مدتی بود برای فیلم و سریال از سرویسهای آماده استفاده میکردم تا اینکه کمکم دردسر استفاده ازشون بیشتر از راحتیش شد.
از اون طرف NAS داشتم و یه سوال ساده برام پیش اومد؛ چرا وقتی میشه کل این جریان رو روی سیستم خودم اجرا کنم، همچنان وابسته به چندتا سرویس مختلف باشم؟
برای همین یه Docker Compose جمع کردم و چندتا ابزار Open Source که هر کدوم یه کار مشخص رو خوب انجام میدن کنار هم گذاشتم:
- Sonarrبرای سریالها
- Radarrبرای فیلمها
- Prowlarr برای مدیریت Indexerها
- Bazarr برای Subtitle
- qBittorrent برای Download Client
- FlareSolverr برای بعضی Sourceهایی که نیازش دارن
- Plex برای Libraryو پخش مدیا
چیزی که برام جالب بود اینه که برای حجم کاریای که انجام میدن Stack خیلی سنگینی نیست. یعنی لازم نیست حتماً یه Server عجیبوغریب داشته باشید و روی NAS، Mini PC یا حتی Raspberry Pi ــ اگر Imageهای ARM64 سرویسها موجود باشه ــ قابل اجراست.
البته Plex Transcoding، مخصوصاً برای 4K، داستان خودش رو داره و اونجا قدرت CPU/GPU دستگاه مهم میشه.
همهچیز داشت کار میکرد ولی یه مشکل وجود داشت؛ برای انجام یه کار ساده باید بین چندتا پنل مختلف میچرخیدم.
برای فیلم Radarr، برای سریال Sonarr، برای Downloadها qBittorrent، برای Indexerها Prowlarr و برای دیدن Library همPlex.
اینجا بود که گفتم چرا یه لایه روی همه اینها نذارم؟
و این شد شروع MediaHub.
MediaHub قرار نیست Sonarr و Radarrرو دوباره بسازه
از اول هدفم این نبود که برم Sonarr یا Radarr یا Plex رو از صفر پیاده کنم.
هر کدوم از این ابزارها سالهاست روی یه مسئله مشخص کار کردن و همون کار رو هم خوب انجام میدن.
MediaHub صرفاً یه Control Planeروی همه اینهاست.
یعنی یه UI واحد که از داخلش بتونم وضعیت سرویسها رو ببینم، فیلم یا سریال Search کنم، دانلودهای در حال انجام رو چک کنم، Library رو ببینم و Notification بگیرم.
نسخه فعلی تقریباً این ساختار رو داره:
MediaHub
├── Dashboard
├── Search & Add
├── Downloads
├── Library
└── SettingsDashboard وضعیت Sonarr، Radarr، Prowlarr، Bazarr، qBittorrent و Plexرو نشون میده.
یه باگ جالب هم همینجا داشتم.
Plexجواب:
HTTP 401میداد ولی MediaHub با افتخار سبزش کرده بود :)
در صورتی که 401 یعنی سرویس Reachable هست ولی درخواست Authenticate نشده.
پس Health Check درست نباید فقط بگه «سرویس جواب داد یا نه».
باید بین این حالتها تفاوت بذاره:
Healthy
Reachable but unauthorized
Unavailableاینم شد یکی از اولین TODOهای پروژه.
جریان دانلود دقیقاً چطوریه؟
در حالت عادی مسیر یه Request تقریباً این شکلیه:
MediaHub
│
▼
Sonarr / Radarr
│
▼
Prowlarr / Indexers
│
▼
Sonarr / Radarr
│
▼
qBittorrent
│
▼
/data/torrents
│
▼
Sonarr / Radarr Import
│
├── /data/media/movies
└── /data/media/tv
│
▼
Plexیعنی Prowlarr خودش چیزی دانلود نمیکنه.
Prowlarr Indexerها رو مدیریت میکنه و Sonarr یا Radarr بعد از پیدا کردن Release مناسب Job رو به qBittorrentمیفرستن.
بعد از تموم شدن دانلود، Sonarr یا Radarr فایل رو Import و مرتب میکنن و Plex هم همون Library رو میخونه.
اجرای پروژه باDocker
برای Development روی سیستمLocal:
git clone <repo-url>
cd mediahubبعد:
docker compose up -d --buildبرای دیدن وضعیت سرویسها:
docker compose psبرایLog:
docker compose logs -fیا مثلاً فقطMediaHub:
docker compose logs -f mediahubبرای Stop کردنStack:
docker compose downو اگر سورس یا Imageها تغییر کرده باشن:
docker compose up -d --buildاولین بار که Stack رو روی Mac بالا آوردم یه مشکل ساده ولی جالب داشتم.
Compose اولیه برای NAS نوشته شده بود و Pathهایی شبیه این داشت:
/volume1/media
/volume1/downloadsکه طبیعتاً روی macOS وجود ندارن.
برای Development تبدیلش کردم به:
MEDIA_ROOT=./data/media
DOWNLOADS_ROOT=./data/torrents
CONFIG_ROOT=./data/configو Directoryها رو ساختم:
mkdir -p \
data/media/movies \
data/media/tv \
data/torrents \
data/configVolumeها یه نکته مهم دارن
نسخه اول برای qBittorrent یه /downloads داشتم، برای Radarr یه /movies و برای Sonarr هم /tv.
کار میکنه ولی ساختار خوبی برای نسخه نهایی نیست.
ساختاری که الان ترجیح میدم اینه:
/data
├── torrents
│ ├── movies
│ └── tv
└── media
├── movies
└── tvو همه Containerهایی که لازم دارن یه /data مشترک میبینن.
مثلاً:
services:
qbittorrent:
volumes:
- /srv/mediahub/data:/data
radarr:
volumes:
- /srv/mediahub/data:/data
sonarr:
volumes:
- /srv/mediahub/data:/dataqBittorrentفایلها رو میریزه داخل:
/data/torrentsRoot Folder فیلم توی Radarrمیشه:
/data/media/moviesو Root Folder سریال تویSonarr:
/data/media/tvمزیت این ساختار اینه که qBittorrent و Sonarr/Radarr فایلها رو با Path یکسان میبینن و امکان Hardlink و Move درست فایلها حفظ میشه.
وقتی Library چند صد گیگ یا چند ترابایت بشه این موضوع دیگه یه جزئیات کوچیک نیست.
اگر بخوام روی یه Linux Server نصبش کنم
فرض کنیم یه Home Server یا Mini PC با Ubuntu/Debian داریم.
اول Directoryها:
sudo mkdir -p /srv/mediahub/data/torrents
sudo mkdir -p /srv/mediahub/data/media/movies
sudo mkdir -p /srv/mediahub/data/media/tv
sudo mkdir -p /srv/mediahub/configبعدPermission:
sudo chown -R $USER:$USER /srv/mediahubداخل .env:
TZ=Asia/Tehran
MEDIA_ROOT=/srv/mediahub/data/media
DOWNLOADS_ROOT=/srv/mediahub/data/torrents
CONFIG_ROOT=/srv/mediahub/configبعد:
docker compose up -d --buildو برای چک کردن:
docker compose psبرای Server دائمی هم بهتره سرویسها:
restart: unless-stoppedداشته باشن تا بعد از Reboot دوباره بالا بیان.
اگر Sonarr و Radarr از قبل نصب باشن چی؟
این حالت برای خودم مهم بود چون ممکنه کسی از قبل Arr Stack خودش رو داشته باشه و فقط بخواد MediaHub رو اضافه کنه.
لازم نیست همه چیز دوباره نصب بشه.
یه Docker Network مشترک میسازیم:
docker network create media-stackبعد سرویسهای موجود و MediaHub رو روی همون Network قرار میدیم:
networks:
media-stack:
external: trueمثلاً:
services:
sonarr:
networks:
- media-stack
radarr:
networks:
- media-stackوMediaHub:
services:
mediahub:
networks:
- media-stack
networks:
media-stack:
external: trueبعد داخل MediaHub نیازی به IP ندارم.
مستقیم میشه نوشت:
http://sonarr:8989
http://radarr:7878
http://prowlarr:9696
http://qbittorrent:8080این یکی از چیزهایی بود که اول خودمم اشتباه کردم و گذاشته بودم:
localhost:7878در حالی که داخل Docker هر Container یه localhost مخصوص خودش داره.
Docker DNS اسم Serviceها رو Resolve میکنه و همین خیلی از دردسرهای IPثابت رو حذف میکنه.
API Keyها
Sonarr، Radarr، Prowlarr و Bazarr از داخل Settings خودشون API Keyمیدن.
MediaHub هم تقریباً با همچین Configای باهاشون حرف میزنه:
SONARR_URL=http://sonarr:8989
SONARR_API_KEY=...
RADARR_URL=http://radarr:7878
RADARR_API_KEY=...
PROWLARR_URL=http://prowlarr:9696
PROWLARR_API_KEY=...
BAZARR_URL=http://bazarr:6767
BAZARR_API_KEY=...qBittorrent هم در تنظیم معمول از Username و Password مربوط به Web UIاستفاده میکنه:
QBITTORRENT_URL=http://qbittorrent:8080
QBITTORRENT_USERNAME=admin
QBITTORRENT_PASSWORD=...و طبیعتاً .env نباید بره داخلGit:
.envProwlarr و FlareSolverr
بعد Prowlarr رو به Sonarr و Radarr وصل کردم تا Indexerها از یه جا مدیریت بشن.
داخل Prowlarr برایSonarr:
Settings
→ Apps
→ SonarrوURL:
http://sonarr:8989برای Radarr هم:
http://radarr:7878بعد Indexerهایی که اجازه استفاده ازشون رو دارم توی Prowlarr تعریف میشن و تنظیمات مرتبط با Sonarr و Radarr Sync میشه.
بعضی Sourceها هم پشت Cloudflare هستن.
برای همین FlareSolverr رو هم داخل Stack گذاشتم.
مسیرش تقریباً این شکلی میشه:
Prowlarr
│
▼
FlareSolverr
│
▼
Sourceالبته FlareSolverr جادو نیست و قرار نیست هر Cloudflare Challenge جدیدی رو حل کنه.
برای بعضی Sourceها کار میکنه، برای بعضیها نه.
Telegram Notification
یه چیزی که خیلی دوست داشتم این بود که بعد از Request کردن یه فیلم یا سریال دیگه لازم نباشه برگردم Dashboard و وضعیتش رو چک کنم.
برای همین Telegram رو هم بهش وصل کردم.
از BotFather یه Bot ساختم و یه Bot Token گرفتم.
Config:
TELEGRAM_BOT_TOKEN=123456789:AAExampleToken
TELEGRAM_CHAT_ID=123456789
WEBHOOK_SECRET=some-random-secretبعد Sonarr و Radarr موقع Download/Import شدن فایل یه Webhook به MediaHub میزنن.
برای مثال نسخه خیلی ساده ارسال پیام باPython:
import os
import requests
TELEGRAM_BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
TELEGRAM_CHAT_ID = os.environ["TELEGRAM_CHAT_ID"]
def send_telegram(message: str):
response = requests.post(
f"https://api.telegram.org/"
f"bot{TELEGRAM_BOT_TOKEN}/sendMessage",
json={
"chat_id": TELEGRAM_CHAT_ID,
"text": message,
},
timeout=10,
)
response.raise_for_status()و برای گرفتن Webhookها میشه خیلی ساده FastAPI گذاشت:
import os
import requests
from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
TELEGRAM_BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
TELEGRAM_CHAT_ID = os.environ["TELEGRAM_CHAT_ID"]
WEBHOOK_SECRET = os.environ["WEBHOOK_SECRET"]
def send_telegram(message: str):
response = requests.post(
f"https://api.telegram.org/"
f"bot{TELEGRAM_BOT_TOKEN}/sendMessage",
json={
"chat_id": TELEGRAM_CHAT_ID,
"text": message,
},
timeout=10,
)
response.raise_for_status()
def check_secret(token: str | None):
if token != WEBHOOK_SECRET:
raise HTTPException(status_code=401)
@app.post("/webhooks/radarr")
async def radarr_webhook(
request: Request,
token: str | None = None,
):
check_secret(token)
payload = await request.json()
if payload.get("eventType") == "Download":
title = payload.get("movie", {}).get(
"title",
"Unknown movie",
)
send_telegram(
f"🎬 {title} downloaded and imported."
)
return {"ok": True}
@app.post("/webhooks/sonarr")
async def sonarr_webhook(
request: Request,
token: str | None = None,
):
check_secret(token)
payload = await request.json()
if payload.get("eventType") == "Download":
title = payload.get("series", {}).get(
"title",
"Unknown series",
)
send_telegram(
f"📺 {title} downloaded and imported."
)
return {"ok": True}برای اجرا:
pip install fastapi uvicorn requestsبعد:
uvicorn main:app \
--host 0.0.0.0 \
--port 8000Webhookها میشن مثلاً:
http://mediahub:8000/webhooks/radarr?token=SECRET
http://mediahub:8000/webhooks/sonarr?token=SECRETو همین URLها داخل Sonarr و Radarr قرار میگیرن.
در نهایت مسیر Notification میشه:
Download finished
│
▼
Sonarr / Radarr
│
▼
MediaHub Webhook
│
▼
Telegram Bot API
│
▼
📱 Notificationیعنی یه فیلم رو Request میکنم، لپتاپ رو میبندم و وقتی Import شد Telegram بهم خبر میده.
برای MVP همین مدل Token توی URL جواب میده ولی برای نسخه Production ترجیح میدم Secret با Header فرستاده بشه و Webhook هم پشت شبکه داخلی یا Reverse Proxy قرار بگیره.
Plex
بعد از Import شدن فایل، Plex از:
/data/media/movies
/data/media/tvLibraryرو میخونه.
برای دسترسی MediaHub به API مربوط به Plex هم Token دارم:
PLEX_URL=http://plex:32400
PLEX_TOKEN=...و قسمت Recently Added هم از همینجا پر میشه.
نسخه نهایی MediaHub قراره چه شکلی باشه؟
نسخه فعلی بیشتر Admin Panel هست.
ولی چیزی که دوست دارم در نهایت داشته باشم یه Media App واقعیتره.
یعنی کاربر وارد بشه، Posterها رو ببینه، Search فارسی و انگلیسی داشته باشه، صفحه Details فیلم و سریال داشته باشه و در نهایت فقط:
Requestرو بزنه.
اگر لازم داشت Quality رو هم انتخاب کنه:
1080p
2160p
HEVCو تمام.
کاربر لازم نیست بدونه پشت صحنه Sonarr، Radarr، Prowlarr یا qBittorrent وجود دارن.
کل پیچیدگی باید پشت MediaHub مخفی بشه.
بحثSecurity
یه موضوع دیگه هم اینه که اگر Stack روی Server اجرا بشه من شخصاً این Portها رو مستقیم Public نمیکنم:
8989
7878
9696
8080
6767Sonarr، Radarr، Prowlarr و qBittorrent پنلهای Adminهستن.
برای دسترسی از بیرون ترجیحم LAN، VPN مثل WireGuard/Tailscale یا Reverse Proxy با Authentication هست.
در حالت ایدهآل MediaHub تنها UIای میشه که کاربر عادی میبینه و بقیه سرویسها فقط داخل Network داخلی با هم حرف میزنن.
آخرش
MediaHub هنوز MVPهست.
Authentication، User Management، مدیریت بهتر Quality Profile، Watchlist، Plex Integration بهتر و UIکاملتر هنوز مونده.
ولی چیزی که برای خودم توی این پروژه جذابه اینه که قرار نیست دوباره چرخ رو اختراع کنم.
چندتا ابزار Open Source که هر کدوم یه کار رو خیلی خوب انجام میدن کنار هم قرار گرفتن و MediaHub فقط اونها رو به یه تجربه واحد تبدیل میکنه.
رایگان، Self-hosted، قابل اجرا روی NAS یا Home Server و مهمتر از همه چیزی که خودم واقعاً ازش استفاده میکنم.
فعلاً نسخه Development روی Mac بالا اومده و کل Stack داره کار میکنه.
مرحله بعد انتقال کاملش روی NAS، بهتر کردن UI و تبدیل کردنش از یه Control Panel ساده به یه Media App واقعیه.