SSD Nodes Learn Hosting plans →
راهنماها Matt Connorتوسط Matt Connor

آموزش نصب Halcyon برای تبدیل Jellyfin به فروشگاه ویدیو

با استفاده از این راهنما، کتابخانه Jellyfin خود را به یک فروشگاه ویدیوی دهه 90 تبدیل کنید. دستور Docker، تنظیمات Reverse Proxy و نکات مهم برای اجرای موفق آن را بخوانید.

آنچه Halcyon با کتابخانه Jellyfin شما انجام می‌دهد

Halcyon Video کتابخانه Jellyfin شما را به شکل یک فروشگاه ویدیوی دهه 1990 که قابلیت قدم زدن در آن وجود دارد، در مرورگر بازسازی می‌کند. هر فیلمی که دارید به یک جعبه روی قفسه تبدیل می‌شود. شما در میان راهروها زیر نور چراغ‌های مهتابی قدم می‌زنید، یک جعبه را از قفسه برمی‌دارید، آن را می‌چرخانید تا مشخصات پشت آن را بخوانید و برای شروع پخش، آن را به پیشخوان می‌برید. وضعیت شروع، پیشرفت و توقف پخش به Jellyfin گزارش می‌شود، بنابراین نقاط ادامه پخش و تاریخچه تماشا دقیق باقی می‌مانند.

Halcyon یک سرور Jellyfin موجود را از طریق API مربوط به Jellyfin می‌خواند و کتابخانه مستقلی برای خود ندارد. این راهنما فرض می‌کند که Jellyfin از قبل در حال اجراست و اسکن کتابخانه را به‌درستی انجام می‌دهد. اگر چنین نیست، ابتدا Jellyfin را به عنوان یک مدیا سرور روی VPS راه‌اندازی کنید و پس از آنکه کتابخانه شما در کلاینت وب معمولی به‌درستی نمایش داده شد، به اینجا بازگردید. این ابزاری است که آن را نصب می‌کنید چون کتابخانه از قبل آماده است، نه به این دلیل که به سرویس دیگری در لیست self-hosting خود نیاز داشته‌اید.

این پروژه تحت مجوز GPL-3.0 است و توسط یک نفر نوشته شده است؛ فایل README به‌صراحت اعلام می‌کند که هیچ pull requestای را نمی‌پذیرد. توسعه پروژه سریع است و هیچ نگهدارنده دومی برای رفع رگرسیون‌ها (regression) وجود ندارد، بنابراین پیش از آنکه فروشگاه را به کسی نشان دهید، نسخه image را ثابت (pin) کنید. بخش آخر نحوه انجام این کار را توضیح می‌دهد.

رندرینگ در کجا انجام می‌شود؟

در مرورگر. Halcyon یک برنامه Vite و TypeScript است که با استفاده از three.js ساخته شده؛ کتابخانه‌ای جاوااسکریپتی که گرافیک‌های سه‌بعدی را از طریق WebGL (رابط مرورگر با GPU) ترسیم می‌کند. هندسه فروشگاه و تصاویر جعبه‌ها توسط دستگاهی که نمایشگر را در اختیار دارد، ترکیب (composite) می‌شوند.

کانتینر کار بسیار کمی انجام می‌دهد. این کانتینر npm run serve را اجرا می‌کند که همان vite preview --port 1420 --strictPort --host است و فایل‌های ساخته‌شده (build) را به همراه چند مسیر میان‌افزار (middleware) کوچک ارائه می‌دهد. Halcyon هیچ‌گونه transcoding اضافه نمی‌کند و هیچ موتوری را روی سرور اجرا نمی‌کند.

بنابراین، مسئله GPU به کلاینت مربوط می‌شود. یک VPS کوچک به‌راحتی از پس این کار برمی‌آید، زیرا ارائه آن به معنای ارسال فایل‌های ایستا از طریق HTTP است. لپ‌تاپ، تبلت یا تلویزیونی که مرورگر را اجرا می‌کند، تعیین‌کننده این است که آیا فروشگاه روان حرکت می‌کند یا با کندی مواجه می‌شود.

یک قابلیت از این قاعده مستثنی است. Remote Play نمونه‌های headless از Chromium را روی سرور ایجاد می‌کند و فروشگاه رندر شده را از طریق WebRTC (ارتباط بلادرنگ وب) به گوشی یا ست‌تاپ‌باکس استریم می‌کند. این مسیر روی سرور رندر می‌شود و به‌صورت پیش‌فرض به دو نمونه محدود شده است که با REMOTE_PLAY_MAX_INSTANCES قابل تنظیم است. بدون نگاشت یک دستگاه /dev/dri، این نمونه‌ها روی CPU رندر می‌شوند؛ بنابراین یک VPS با دو هسته، فشار هر بیننده اضافه را کاملاً حس خواهد کرد.

آنچه فروشگاه از کتابخانه شما می‌خواند

قفسه‌ها از ساختار داخلی Jellyfin سرچشمه می‌گیرند. Halcyon بخش‌ها را بر اساس کتابخانه‌ها و ژانرهای شما می‌چیند و دنباله‌ها را از BoxSets گروه‌بندی می‌کند. مشخصات چاپ‌شده در پشت هر قاب، از متادیتای MediaStreams که Jellyfin از قبل در اختیار دارد استخراج می‌شود؛ این یعنی هر موردی که در Jellyfin وجود نداشته باشد، در قفسه نیز دیده نخواهد شد.

این موضوع باعث می‌شود فروشگاه آینه‌ای دقیق از متادیتای شما باشد. کتابخانه‌ای که توسط یک stack از arr در Docker Compose با کاورها و ژانرهای تکمیل‌شده تغذیه می‌شود، در اینجا بسیار بهتر از پوشه‌ای شامل فایل‌های پراکنده با نام‌های عمومی به نظر می‌رسد. کتابخانه‌های عکس نیز وابستگی مشابهی به ابزاری دارند که آن‌ها را ایندکس کرده است؛ نکته‌ای که هنگام مقایسه PhotoPrism در برابر Immich برای تصاویر ثابت موجود روی همان سرور، ارزش به خاطر سپردن دارد.

پیش از نصب هر چیزی، دموی فروشگاه ویدیو را امتحان کنید

این پروژه کل فروشگاه را با استفاده از یک کتابخانه مصنوعی در دموی میزبانی‌شده منتشر می‌کند. افزودن ?demo=1 به انتهای هر URL در Halcyon، همین کار را در محیط مستقرشده توسط خودتان انجام می‌دهد.

از این دمو به عنوان تست سخت‌افزار استفاده کنید، با یک نکته مهم: دمو اندازه خود را با دستگاه تطبیق می‌دهد. مرورگری که فضای خالی GPU کافی داشته باشد، کل فروشگاه شامل حدود 2,000 عنوان را دریافت می‌کند. یک GPU متوسط حدود 800 عنوان را بارگذاری می‌کند. گوشی‌های هوشمند یا هر مرورگری که حافظه دستگاه را 4 GB یا کمتر گزارش کند، حدود 300 عنوان دریافت می‌کند. دمویی که روی یک دستگاه ضعیف به‌خوبی اجرا می‌شود، قبلاً اندازه خود را کوچک کرده است؛ بنابراین اطلاعات کمی درباره نحوه اجرای کتابخانه کامل شما در آن دستگاه ارائه می‌دهد. تست مفید، استفاده از دستگاهی است که کل فروشگاه را دریافت می‌کند. اگر در آن حالت لرزش (stutter) مشاهده کردید، کتابخانه شخصی شما با اندازه مشابه نیز دچار لرزش خواهد شد و راه‌حل آن، استفاده از حالت 2.5D است که در ادامه توضیح داده شده، نه ارتقای VPS به یک مدل قوی‌تر.

اجرا با Docker

این دستوری است که مستندات بالادستی ارائه کرده است.

docker run -d --name halcyon --network host --restart unless-stopped \
  ghcr.io/halcyon-video/halcyon-video

سپس بررسی کنید که سرویس بالا آمده باشد.

docker logs halcyon
curl -I http://127.0.0.1:1420

لاگ باید نشان دهد که سرور پیش‌نمایش روی پورت 1420 در حال گوش دادن است و curl باید به HTTP/1.1 200 OK پاسخ دهد. کانتینری که پس از چند ثانیه خارج می‌شود، تقریباً همیشه با مشکل پورت مواجه است. --strictPort به این معنی است که سرور وقتی پورت 1420 اشغال است، از انتقال به 1421 خودداری می‌کند و در نتیجه متوقف می‌شود.

--network host برای Remote Play است، نه برای فروشگاه. WebRTC باید آدرس واقعی دستگاه را به دستگاهی که قصد دریافت استریم دارد، اعلام کند. در پشت bridge پیش‌فرض Docker، کانتینر فقط آدرس 172.x خود را می‌شناسد که هیچ گوشی در شبکه شما به آن دسترسی ندارد، بنابراین استریم هرگز متصل نمی‌شود. اگر فقط فروشگاه را در مرورگر می‌خواهید، به جای آن پورت را publish کنید.

docker run -d --name halcyon -p 1420:1420 --restart unless-stopped \
  ghcr.io/halcyon-video/halcyon-video

این گزینه در VPS پیش‌فرض بهتری است، زیرا host networking کانتینر را روی تمام اینترفیس‌های دستگاه، از جمله اینترفیس عمومی، قرار می‌دهد. اجرای Docker روی VPS بقیه این مباحث را پوشش می‌دهد. --restart unless-stopped همان چیزی است که فروشگاه را پس از reboot برمی‌گرداند؛ مشابه ایده سرویس‌های Compose که در زمان بوت شروع می‌شوند.

کلون کردن مخزن و اجرای docker compose up -d، ایمیج را به‌صورت محلی build می‌کند. فایل Compose موجود به‌صورت پیش‌فرض از سورس build می‌شود و خط image: را به‌صورت کامنت‌شده دارد؛ بنابراین اگر می‌خواهید از ایمیج منتشرشده در Compose استفاده کنید، آن خط را از حالت کامنت خارج کنید.

ایمیج منتشرشده هر دو مورد linux/amd64 و linux/arm64 را پوشش می‌دهد. در تاریخ 9 اکتبر 2026 بررسی شد که نسخه 0.25.0 فعلی، یک digest جداگانه برای هر پلتفرم لیست می‌کند، بنابراین یک VPS با معماری arm64 با همان دستوری pull می‌شود که سایر میزبان‌ها استفاده می‌کنند. محدودیت amd64 کوتاه‌مدت بود: changelog در تاریخ 8 اوت 2026 در نسخه v0.2.1 ایمیج arm64 را حذف کرد و در تاریخ 11 اوت 2026 در نسخه v0.4.0، با build روی runnerهای بومی، آن را بازگرداند. فقط میزبانی که روی تگی از آن بازه زمانی قفل شده باشد، نیاز به build از روی کلون دارد.

اتصال به سرور Jellyfin

برنامه http://<host>:1420 را باز کرده و با آدرس سرور Jellyfin، نام کاربری و رمز عبور خود وارد شوید. فایل .env.local.example موجود در مخزن، صرفاً برای توسعهٔ محلی است. Vite متغیرهایی را که با VITE_ شروع می‌شوند در کد سمت کلاینت قرار می‌دهد؛ بنابراین رمز عبور Jellyfin که در آنجا نوشته شود، در بستهٔ JavaScript که هر بازدیدکننده دانلود می‌کند، کامپایل خواهد شد. روی سروری که برای دیگران قابل دسترسی است، حتماً از طریق رابط کاربری وارد شوید.

مرورگر مستقیماً با Jellyfin ارتباط برقرار می‌کند. کانتینر Halcyon درخواست‌های API مربوط به Jellyfin را پروکسی نمی‌کند و این موضوع دو پیامد دارد که پیش از شروع عیب‌یابی باید از آن‌ها آگاه باشید.

نخست اینکه Jellyfin باید از طریق مرورگر قابل دسترسی باشد، نه فقط از طریق VPS که Halcyon را میزبانی می‌کند. اگر Jellyfin روی 127.0.0.1:8096 تنظیم شده باشد، برای تست محلی مناسب است اما باعث می‌شود قفسه‌ها برای سایر کاربران خالی بماند.

دوم اینکه این فراخوانی از نوع cross-origin است، یعنی از آدرس Halcyon به آدرس Jellyfin انجام می‌شود. Jellyfin به‌صورت پیش‌فرض به درخواست‌های API با Access-Control-Allow-Origin: * پاسخ می‌دهد، بنابراین بدون نیاز به پیکربندی اضافی کار می‌کند. اگر این تنظیم را محدود کرده‌اید یا یک پروکسی احراز هویت در مقابل API مربوط به Jellyfin قرار داده‌اید، کنسول مرورگر خطای blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource را گزارش می‌دهد و فروشگاه با قفسه‌های خالی بارگذاری می‌شود.

قرار دادن آن پشت یک reverse proxy، همراه با احراز هویت در لایه اول

vite preview یک سرور پیش‌نمایش است. این سرویس هیچ TLS (امنیت لایه انتقال) را خاتمه نمی‌دهد و کنترل دسترسی داخلی ندارد، بنابراین در هر محیط عمومی باید پشت nginx یا Caddy قرار گیرد.

server {
  listen 443 ssl;
  server_name halcyon.example.com;

  location / {
    proxy_pass http://127.0.0.1:1420;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}

یک نام دامنه که در مقابل کانتینر قرار می‌گیرد، به یک تنظیم اضافه نیاز دارد. Halcyon برای محافظت در برابر DNS rebinding، به localhost، آدرس‌های IP خام و نام ماشینی که روی آن اجرا می‌شود پاسخ می‌دهد. در داخل کانتینر، ماشینی که روی آن اجرا می‌شود همان کانتینر است، بنابراین hostname آن با hostname شما متفاوت است. درخواستی که با نام halcyon.example.com می‌رسد رد می‌شود و پاسخ شامل نام میزبانی است که درخواست را رد کرده است. آن نام را اضافه کنید.

docker run -d --name halcyon -p 127.0.0.1:1420:1420 --restart unless-stopped \
  -e HALCYON_ALLOWED_HOSTS=halcyon.example.com \
  ghcr.io/halcyon-video/halcyon-video

مقداردهی با کاما جدا می‌شود، یک نقطه در ابتدا مانند .example.com با زیردامنه‌ها مطابقت دارد و all بررسی را غیرفعال می‌کند. تنها زمانی از all استفاده کنید که ماشین در دسترس هیچ منبع خارجی نباشد.

هنگامی که فروشگاه از طریق https:// ارائه می‌شود، آدرس Jellyfin که هنگام ورود تایپ می‌کنید نیز باید https:// باشد. مرورگر یک فراخوانی API از نوع http:// را که از یک صفحه HTTPS انجام شده باشد مسدود می‌کند و کنسول خطای Mixed Content: The page at 'https://halcyon.example.com/' was loaded over HTTPS, but requested an insecure resource را نمایش می‌دهد. ورود به سادگی شکست می‌خورد، بدون اینکه توضیحی در داخل Halcyon ارائه شود. هر دو را از طریق TLS ارائه دهید، یا هر دو را در یک شبکه خصوصی روی HTTP ساده نگه دارید.

سپس نوبت به احراز هویت می‌رسد. فروشگاه درخواست اعتبارنامه Jellyfin می‌کند، بنابراین غریبه‌ای که URL را پیدا کند با صفحه ورود مواجه می‌شود. یک ویژگی این وضعیت را تغییر می‌دهد. فعال کردن Remote Play در بخش Settings و سپس Connection، نشست Jellyfin شما را به سرور منتقل می‌کند تا بازدیدکنندگان /remote.html نمونه‌ای از کتابخانه واقعی شما را دریافت کنند. این هدف اصلی این ویژگی است و به این معنی است که محرمانگی URL تنها چیزی است که بین اینترنت و فیلم‌های شما قرار دارد. اگر Remote Play را فعال می‌کنید، با استفاده از Authentik به عنوان یک درگاه SSO خودمیزبان، احراز هویت یکپارچه (SSO) را در مقابل کل سایت قرار دهید، یا نام میزبان عمومی را حذف کرده و از طریق یک تونل WireGuard مدیریت‌شده با wg-easy به فروشگاه دسترسی پیدا کنید.

دو جزئیات در این مورد وجود دارد. reverse proxy فقط فروشگاه را هدایت می‌کند: استریم Remote Play از نوع WebRTC روی UDP است و از طریق HTTP proxy عبور نمی‌کند، بنابراین هنگام استفاده از TURN relay داخلی، به مسیر اختصاصی خود روی پورت 3478/udp و محدوده 49200 تا 49260/udp نیاز دارد. همچنین، docker run ساده در بالا هیچ volumeای را نگه نمی‌دارد، بنابراین seed مربوط به Remote Play پس از docker rm باقی نمی‌ماند. فایل Compose دقیقاً به همین دلیل یک volume از نوع halcyon-data را در مسیر /data mount می‌کند و REMOTE_PLAY_SEED را روی /data/remote-play-seed.json تنظیم می‌کند.

هنگامی که فروشگاه به درستی کار نمی‌کند چه باید کرد

Halcyon رندر را به‌صورت درخواستی انجام می‌دهد. یک فروشگاه غیرفعال هیچ فریمی تولید نمی‌کند و از دست رفتن فوکوس پنجره، حلقه انیمیشن را متوقف می‌کند؛ به همین دلیل است که باز ماندن یک تب باعث تخلیه باتری لپ‌تاپ نمی‌شود. این ویژگی به دستگاه‌هایی که در مرز توانایی سخت‌افزاری هستند کمک می‌کند، اما برای دستگاهی که اصلاً قادر به ترسیم فروشگاه نیست، تأثیری ندارد.

برای چنین کلاینت‌هایی، یک حالت 2.5D وجود دارد که شامل HTML و CSS ساده بدون WebGL است و برای سخت‌افزارهای ضعیفی مانند Raspberry Pi در نظر گرفته شده است. شما می‌توانید بدون بارگذاری مجدد صفحه، از طریق تنظیمات یا منوی پاور بین حالت 3D و 2.5D جابه‌جا شوید، بنابراین تست هر دو حالت روی یک دستگاه تنها چند ثانیه زمان می‌برد. در مورد خروجی واقع‌بین باشید: نویسنده این حالت تخت (flat) را خام و در حال توسعه توصیف می‌کند. آن را به عنوان یک جایگزین (fallback) برای کلاینت‌های ضعیف در نظر بگیرید.

هنگامی که یک کلاینت برای فروشگاه 3D بیش از حد ضعیف باشد، خرابی به‌وضوح مشخص است. تب به‌طور خودکار بارگذاری مجدد می‌شود یا مرورگر گزارش از دست رفتن WebGL context را می‌دهد، که معمولاً در حین پر شدن قفسه‌ها رخ می‌دهد. در چنین شرایطی، به‌جای کاهش حجم کتابخانه خود، آن دستگاه را به حالت 2.5D منتقل کنید.

تصویر را ثابت (Pin) کنید و پیش از pull کردن بررسی انجام دهید

این بخش را جدی بگیرید. تگ‌های v0.1.0 تا v0.3.1 همگی در فاصله چند روز از یکدیگر منتشر شدند و v0.2.1 تنها به این دلیل وجود دارد که push تصویر برای v0.2.0 با شکست مواجه شد. سرعت انتشار از آن زمان کاهش نیافته است: v0.25.0 در تاریخ 7 اکتبر 2026، یعنی سه روز پس از v0.23.0 منتشر شد. گزارش‌های باگ در مخزن اصلی (upstream) پذیرفته می‌شوند، اما وصله‌ها (patches) خیر؛ بنابراین جریان انتشار، وضعیت کاری یک فرد است.

اجرای latest با عادتِ استفاده از docker pull به این معنی است که فروشگاه (store) می‌تواند در هر سه‌شنبه معمولی زیر پای شما تغییر کند. با استفاده از digest، که تنها مرجعی است که تغییر نمی‌کند، آن را ثابت (Pin) کنید.

docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.25.0

این دستور digest پشت تگ را چاپ می‌کند. از آن به جای تگ استفاده کنید.

docker run -d --name halcyon -p 1420:1420 --restart unless-stopped \
  ghcr.io/halcyon-video/halcyon-video@sha256:7fc00e619a30004d275faa79ad36d810e5a8530757132603065ba4e848dd67de

آن digest در تاریخ 9 اکتبر 2026 برابر با 0.25.0 بود. به جای کپی کردن، خودتان digest فعلی را بخوانید و پیش از تغییر، یادداشت‌های انتشار (release notes) را مطالعه کنید، زیرا یک نسخه وصله (patch release) در اینجا می‌تواند علاوه بر اصلاحات، تغییراتی در ساختار فروشگاه نیز به همراه داشته باشد.

FAQ

آیا Halcyon به GPU روی VPS من نیاز دارد؟

برای استفاده عادی خیر. فروشگاه توسط three.js در مرورگر رندر می‌شود، بنابراین دستگاه کلاینت عملیات رندر را انجام می‌دهد و کانتینر فقط فایل‌های استاتیک را روی پورت 1420 سرو می‌کند. استثنا در مورد Remote Play است که یک نسخه headless از Chromium را روی سرور اجرا کرده و نتیجه را استریم می‌کند. این مسیر روی CPU رندر می‌شود، مگر اینکه /dev/dri را برای شتاب‌دهنده سخت‌افزاری به داخل کانتینر نگاشت (map) کنید.

آیا می‌توانم Halcyon را روی اینترنت عمومی قرار دهم؟

فقط در صورتی که پشت یک لایه احراز هویت باشد. فروشگاه درخواست اعتبارنامه Jellyfin می‌کند، اما فعال کردن Remote Play باعث می‌شود نشست (session) Jellyfin شما به سرور منتقل شود؛ بنابراین هر کسی که /remote.html را بارگذاری کند، بدون نیاز به ورود، به نمونه‌ای از کتابخانه واقعی شما دسترسی پیدا می‌کند. یک reverse proxy با قابلیت single sign on جلوی آن قرار دهید، یا نام دامنه را از DNS عمومی دور نگه دارید و از طریق VPN به فروشگاه دسترسی پیدا کنید.

چرا پس از ورود، قفسه‌ها خالی هستند؟

مرورگر مستقیماً با API مربوط به Jellyfin تماس می‌گیرد، بنابراین Jellyfin باید از سمت مرورگر قابل دسترسی باشد، نه فقط از سمت VPS. کنسول مرورگر را باز کنید. خطای blocked by CORS policy به این معنی است که Jellyfin درخواست ارسالی از آدرس Halcyon را نمی‌پذیرد. پیام Mixed Content به این معنی است که صفحه روی HTTPS است، در حالی که آدرس Jellyfin که وارد کرده‌اید HTTP ساده است.

آیا به --network host نیاز دارم؟

فقط برای Remote Play. پروتکل WebRTC باید آدرس واقعی دستگاه را تبلیغ کند، و در پشت Docker bridge، کانتینر فقط می‌تواند یک آدرس 172.x ارائه دهد که هیچ گوشی در شبکه شما قادر به دسترسی به آن نیست. برای مرور فروشگاه در مرورگر، -p 1420:1420 کافی است و سطح دسترسی کمتری از میزبان (host) را در معرض قرار می‌دهد.

از کدام image tag باید استفاده کنم؟

به جای latest، یک digest را پین کنید. digest مربوط به نسخه‌ای که دارای docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.25.0 است را بخوانید، همان را اجرا کنید و تنها پس از مطالعه یادداشت‌های انتشار (release notes) به نسخه جدیدتر مهاجرت کنید. تا تاریخ 9 اکتبر 2026، ایمیج منتشر شده شامل هر دو مورد linux/amd64 و linux/arm64 است، بنابراین یک میزبان arm64 آن را مانند هر ایمیج دیگری دریافت می‌کند و نیازی به build از روی clone نیست.