تبدیل کتابخانه Jellyfin به فروشگاه ویدیو با Halcyon
با استفاده از Halcyon کتابخانه Jellyfin خود را به یک فروشگاه ویدیوی دهه 90 تبدیل کنید. در این راهنما دستور Docker، تنظیمات Reverse Proxy و محدودیتهای این پروژه را بررسی میکنیم.
عملکرد Halcyon روی کتابخانه Jellyfin شما
Halcyon Video کتابخانه Jellyfin شما را به شکل یک فروشگاه ویدیوی دهه 1990 که در مرورگر قابل قدم زدن است، بازسازی میکند. هر فیلمی که دارید به یک جعبه روی قفسه تبدیل میشود. شما در راهروها زیر نور مهتابی قدم میزنید، یک جعبه را پایین میکشید، آن را برمیگردانید تا مشخصات پشت آن را بخوانید و برای شروع پخش، آن را به پیشخوان میبرید. گزارشهای شروع، پیشرفت و توقف پخش به Jellyfin ارسال میشوند، بنابراین نقاط ادامه پخش و تاریخچه تماشا دقیق باقی میمانند.
Halcyon یک سرور Jellyfin موجود را از طریق API آن میخواند و کتابخانه مستقلی برای خود ندارد. این راهنما فرض میکند که Jellyfin از قبل در حال اجراست و اسکن کتابخانه را بهدرستی انجام میدهد. اگر اینطور نیست، ابتدا Jellyfin را به عنوان یک مدیا سرور روی VPS راهاندازی کنید و زمانی که کتابخانه شما در کلاینت وب معمولی بهدرستی نمایش داده شد، بازگردید. این ابزاری است که آن را نصب میکنید چون کتابخانه از قبل آماده است، نه به این دلیل که به سرویس دیگری در لیست self-hosting خود نیاز داشتید.
این پروژه تحت مجوز GPL-3.0 است و توسط یک نفر نوشته شده است؛ فایل README بهصراحت اعلام میکند که pull request نمیپذیرد. توسعه پروژه سریع است و هیچ نگهدارنده دومی برای رفع رگرسیونها وجود ندارد، بنابراین پیش از نشان دادن فروشگاه به دیگران، نسخه image را ثابت (pin) کنید. بخش آخر نحوه انجام این کار را پوشش میدهد.
رندرینگ کجا انجام میشود؟
در مرورگر. Halcyon یک برنامه Vite و TypeScript است که با استفاده از three.js ساخته شده؛ یک کتابخانه JavaScript که گرافیکهای سهبعدی را از طریق WebGL (رابط مرورگر با GPU) ترسیم میکند. هندسه فروشگاه و تصاویر جعبهها توسط دستگاهی که نمایشگر به آن متصل است، ترکیب (composite) میشوند.
کانتینر کار بسیار کمی انجام میدهد. این کانتینر npm run serve را اجرا میکند که همان vite preview --port 1420 --strictPort --host است و فایلهای ساختهشده (build) را به همراه چند مسیر میانافزار (middleware) کوچک ارائه میدهد. Halcyon هیچگونه transcoding اضافه نمیکند و هیچ موتوری را روی سرور اجرا نمیکند.
بنابراین، مسئله GPU مربوط به کلاینت است. یک VPS کوچک بهراحتی از پس این کار برمیآید، زیرا ارائه آن به معنای ارسال فایلهای استاتیک از طریق HTTP است. لپتاپ، تبلت یا تلویزیونی که مرورگر را اجرا میکند، تعیینکننده این است که آیا فروشگاه روان حرکت میکند یا با کندی مواجه میشود.
یک ویژگی از این قاعده مستثنی است. Remote Play نمونههای headless از Chromium را روی سرور ایجاد میکند و فروشگاه رندرشده را از طریق WebRTC (ارتباط بیدرنگ وب) به گوشی یا set-top box استریم میکند. این مسیر روی سرور رندر میشود و بهطور پیشفرض به دو نمونه محدود شده است که با REMOTE_PLAY_MAX_INSTANCES قابل تنظیم است. بدون نگاشت یک دستگاه /dev/dri، این نمونهها روی CPU رندر میشوند؛ بنابراین یک VPS با دو هسته، فشار هر بیننده اضافه را کاملاً حس میکند.
آنچه فروشگاه از کتابخانه شما میخواند
قفسهها از ساختار داخلی Jellyfin استخراج میشوند. Halcyon بخشها را بر اساس کتابخانهها و ژانرهای شما چیدمان میکند و دنبالهها را از طریق BoxSets گروهبندی مینماید. مشخصات فنی چاپشده در پشت هر قاب، از متادیتای MediaStreams که Jellyfin از قبل در اختیار دارد تأمین میشود؛ این یعنی هر موردی که در Jellyfin وجود نداشته باشد، در قفسه نیز نمایش داده نخواهد شد.
این موضوع باعث میشود فروشگاه بازتاب دقیقی از متادیتای شما باشد. کتابخانهای که توسط یک استک arr در Docker Compose با کاورها و ژانرهای تکمیلشده تغذیه میشود، در اینجا بسیار بهتر از پوشهای شامل فایلهای پراکنده با نامهای عمومی به نظر میرسد. کتابخانههای عکس نیز وابستگی مشابهی به ابزاری دارند که آنها را ایندکس کرده است؛ نکتهای که هنگام مقایسه PhotoPrism در برابر Immich برای تصاویر ساکن موجود روی همان سرور، ارزش به خاطر سپردن دارد.
پیش از نصب هر چیزی، نسخه نمایشی فروشگاه ویدیو را امتحان کنید
این پروژه کل فروشگاه را که با یک کتابخانه مصنوعی اجرا میشود، در نسخه نمایشی میزبانیشده منتشر کرده است. افزودن ?demo=1 به انتهای هر URL در Halcyon، همین کار را در استقرار شخصی شما انجام میدهد.
از این نسخه به عنوان آزمون سختافزاری استفاده کنید. کتابخانه نمایشی حدود 2,000 عنوان دارد و تقریباً به 2 GB حافظه مرورگر نیاز دارد که از اکثر کتابخانههای شخصی سنگینتر است. اگر نسخه نمایشی روی دستگاهی که قصد دارید از آن استفاده کنید دچار کندی یا وقفه شد، کتابخانه شخصی شما نیز دچار همین مشکل خواهد شد؛ در این صورت، راهحل استفاده از حالت 2.5D است که در ادامه توضیح داده شده، نه ارتقای VPS به سیستمی قویتر.
اجرا با Docker
این دستوری است که در مستندات upstream ذکر شده است.
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 استفاده کنید، این خط را از حالت کامنت خارج کنید.
یک محدودیت جدی تا اوت 2026: ایمیج منتشرشده فقط linux/amd64 است. بخش arm64 در push چندمعماری (multi-architecture) تحت شبیهسازی با شکست مواجه شده و در انتظار runnerهای بومی arm است. روی یک VPS با معماری arm64، عملیات pull با خطای no matching manifest for linux/arm64/v8 in the manifest list entries مواجه میشود و راه حل آن، 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;
}
}یک نام دامنه که جلوی container قرار میگیرد، به یک تنظیم اضافه نیاز دارد. Halcyon برای جلوگیری از DNS rebinding، فقط به localhost، آدرسهای IP خام و نامهای ماشینی که روی آن اجرا میشود پاسخ میدهد. داخل یک container، ماشینی که سرویس روی آن اجرا میشود همان container است، بنابراین 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 عبور نمیکند، بنابراین به مسیر اختصاصی خود روی پورت 3478/udp و محدوده 49200 تا 49260/udp نیاز دارد (زمانی که از TURN relay داخلی استفاده میشود). همچنین، docker run ساده در بالا هیچ volumeای را نگه نمیدارد، بنابراین seed مربوط به Remote Play با docker rm از بین میرود. فایل Compose دقیقاً به همین دلیل یک volume از نوع halcyon-data را در مسیر /data mount میکند و REMOTE_PLAY_SEED را روی /data/remote-play-seed.json تنظیم میکند.
هنگامی که فروشگاه به درستی کار نمیکند چه باید کرد
Halcyon رندر را بهصورت درخواستی انجام میدهد. یک فروشگاه غیرفعال هیچ فریمی را ترکیب (composite) نمیکند و از دست رفتن فوکوس پنجره، حلقه انیمیشن را متوقف میکند؛ به همین دلیل است که باز ماندن یک تب، باتری لپتاپ را خالی نمیکند. این ویژگی به دستگاههایی که در مرز توانایی سختافزاری هستند کمک میکند، اما برای دستگاهی که اصلاً قادر به ترسیم فروشگاه نیست، تأثیری ندارد.
برای این کلاینتها، یک حالت 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 با شکست مواجه شد. گزارشهای باگ در مخزن اصلی پذیرفته میشوند، اما وصلهها (patch) خیر؛ بنابراین جریان انتشار (release stream)، صرفاً وضعیت کاری یک فرد است.
اجرای latest با عادتِ استفاده از docker pull به این معناست که ممکن است در هر سهشنبه معمولی، محتوای مخزن زیر پای شما تغییر کند. با استفاده از digest تصویر را ثابت کنید؛ این تنها مرجعی است که تغییر نمیکند.
docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.3.1این دستور digestِ پشتِ تگ را چاپ میکند. از آن به جای تگ استفاده کنید.
docker run -d --name halcyon -p 1420:1420 --restart unless-stopped \
ghcr.io/halcyon-video/halcyon-video@sha256:747dcc821a3d2fa318b50e76024783c1835609047e84f502e23d021bc1898b20آن digest در تاریخ 10 اوت 2026 برابر با 0.3.1 بود. به جای کپی کردن، خودتان digest فعلی را بخوانید و پیش از هر تغییری، یادداشتهای انتشار (release notes) را مطالعه کنید؛ زیرا یک نسخه وصله (patch release) در اینجا ممکن است علاوه بر اصلاحات، تغییراتی در ساختار ذخیرهسازی نیز به همراه داشته باشد.
FAQ
آیا Halcyon روی VPS من به GPU نیاز دارد؟
برای استفاده عادی خیر. فروشگاه توسط 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) را در معرض قرار میدهد.
از کدام تگ ایمیج باید استفاده کنم؟
به جای latest، یک digest را ثابت (pin) کنید. digest مربوط به نسخهای با docker buildx imagetools inspect ghcr.io/halcyon-video/halcyon-video:0.3.1 را بخوانید، همان را اجرا کنید و تنها پس از مطالعه یادداشتهای انتشار (release notes) به نسخه جدیدتر مهاجرت کنید. تا اوت 2026، ایمیج منتشر شده فقط linux/amd64 است، بنابراین یک میزبان arm64 باید از طریق clone کردن با docker compose up -d اقدام به build کند.