From 870db53b4fde9be08ebfcbc0af67b26ee4496965 Mon Sep 17 00:00:00 2001 From: Shaela Bello Date: Mon, 23 Feb 2026 19:39:59 -0500 Subject: [PATCH] fix(jellyfin): align prod topology and yt metadata path --- pmoves/docker-compose.jellyfin-ai.yml | 29 +++++++++++++++++-- pmoves/docker-compose.yml | 17 +++++++++-- pmoves/env.jellyfin-ai.example | 8 +++++ pmoves/env.shared.example | 1 + pmoves/services/jellyfin-bridge/Dockerfile | 4 +-- .../services/jellyfin-bridge/requirements.txt | 9 +++--- pmoves/services/pmoves-yt/yt.py | 27 ++++++++++++++--- 7 files changed, 79 insertions(+), 16 deletions(-) diff --git a/pmoves/docker-compose.jellyfin-ai.yml b/pmoves/docker-compose.jellyfin-ai.yml index 7345b2f2c7..90549b9507 100644 --- a/pmoves/docker-compose.jellyfin-ai.yml +++ b/pmoves/docker-compose.jellyfin-ai.yml @@ -36,7 +36,7 @@ x-jellyfin-base: &jellyfin-base services: jellyfin: <<: *jellyfin-base - container_name: ${JELLYFIN_CONTAINER_NAME:-pmoves-jellyfin} + container_name: ${JELLYFIN_CONTAINER_NAME:-pmoves-jellyfin-ai} profiles: ["default", "jellyfin-ai"] image: lscr.io/linuxserver/jellyfin:10.11.0 environment: @@ -60,11 +60,12 @@ services: pmoves_app: aliases: - jellyfin-ai + pmoves_external: restart: unless-stopped jellyfin-vaapi: <<: *jellyfin-base - container_name: ${JELLYFIN_CONTAINER_NAME:-pmoves-jellyfin} + container_name: ${JELLYFIN_CONTAINER_NAME:-pmoves-jellyfin-ai} profiles: ["jellyfin-ai-vaapi"] devices: - ${JELLYFIN_VAAPI_DEVICE:-/dev/dri/renderD128}:${JELLYFIN_VAAPI_DEVICE:-/dev/dri/renderD128} @@ -79,7 +80,7 @@ services: jellyfin-nvenc: <<: *jellyfin-base - container_name: ${JELLYFIN_CONTAINER_NAME:-pmoves-jellyfin} + container_name: ${JELLYFIN_CONTAINER_NAME:-pmoves-jellyfin-ai} profiles: ["jellyfin-ai-nvenc"] gpus: ${JELLYFIN_NVIDIA_GPUS:-all} environment: @@ -128,6 +129,7 @@ services: pmoves_app: aliases: - jellyfin-qwen-audio + pmoves_external: restart: unless-stopped # Uncomment for GPU support # deploy: @@ -152,6 +154,14 @@ services: - NEO4J_URI=${JELLYFIN_NEO4J_URI:-bolt://jellyfin-neo4j:7687} - NEO4J_USER=${JELLYFIN_NEO4J_USER:-neo4j} - NEO4J_PASSWORD=${JELLYFIN_NEO4J_PASSWORD:-mediapassword123} + - SUPABASE_URL=${SUPABASE_URL:-http://host.docker.internal:65421} + - SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY:-} + - SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY:-} + - SUPABASE_JWT_SECRET=${SUPABASE_JWT_SECRET:-} + - AUTH_BOOTSTRAP_MODE=${AUTH_BOOTSTRAP_MODE:-jwt} + - NATS_URL=${NATS_URL:-nats://nats:pmoves@nats:4222} + - TENSORZERO_URL=${TENSORZERO_URL:-http://tensorzero-gateway:3000} + - GPU_ORCHESTRATOR_URL=${GPU_ORCHESTRATOR_URL:-http://gpu-orchestrator:8200} - JELLYFIN_HWACCEL_MODE=${JELLYFIN_HWACCEL_MODE:-software} - JELLYFIN_ENABLE_HDR=${JELLYFIN_ENABLE_HDR:-1} - JELLYFIN_PREFER_AV1=${JELLYFIN_PREFER_AV1:-1} @@ -197,6 +207,14 @@ services: - NEO4J_USER=${JELLYFIN_NEO4J_USER:-neo4j} - NEO4J_PASSWORD=${JELLYFIN_NEO4J_PASSWORD:-mediapassword123} - REDIS_URL=${JELLYFIN_REDIS_URL:-redis://jellyfin-redis:6379} + - SUPABASE_URL=${SUPABASE_URL:-http://host.docker.internal:65421} + - SUPABASE_SERVICE_KEY=${SUPABASE_SERVICE_KEY:-${SUPABASE_SERVICE_ROLE_KEY:-}} + - SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY:-} + - SUPABASE_JWT_SECRET=${SUPABASE_JWT_SECRET:-} + - AUTH_BOOTSTRAP_MODE=${AUTH_BOOTSTRAP_MODE:-jwt} + - NATS_URL=${NATS_URL:-nats://nats:pmoves@nats:4222} + - TENSORZERO_URL=${TENSORZERO_URL:-http://tensorzero-gateway:3000} + - GPU_ORCHESTRATOR_URL=${GPU_ORCHESTRATOR_URL:-http://gpu-orchestrator:8200} - JELLYFIN_HWACCEL_MODE=${JELLYFIN_HWACCEL_MODE:-software} - JELLYFIN_ENABLE_HDR=${JELLYFIN_ENABLE_HDR:-1} - JELLYFIN_PREFER_AV1=${JELLYFIN_PREFER_AV1:-1} @@ -206,6 +224,7 @@ services: pmoves_api: aliases: - jellyfin-api + pmoves_external: restart: unless-stopped depends_on: - ${JELLYFIN_STACK_SERVICE:-jellyfin} @@ -229,6 +248,7 @@ services: pmoves_app: aliases: - jellyfin-dashboard + pmoves_external: restart: unless-stopped depends_on: - jellyfin-api-gateway @@ -246,3 +266,6 @@ networks: pmoves_data: external: true name: pmoves_data + pmoves_external: + external: true + name: pmoves_external diff --git a/pmoves/docker-compose.yml b/pmoves/docker-compose.yml index 431682eb31..4d9a47a908 100644 --- a/pmoves/docker-compose.yml +++ b/pmoves/docker-compose.yml @@ -1841,6 +1841,7 @@ services: networks: - pmoves_app - pmoves_bus + - pmoves_external # Required for host reachability + external media metadata lookups pmoves-ollama: <<: *tier-llm-hardened image: ${PMOVES_OLLAMA_IMAGE:-ollama/ollama:0.12.6@sha256:ea37dcbf693248c80796654500fccf1e12f55da20ec7c78c30870a9a227c4cdd} @@ -1889,7 +1890,17 @@ services: - --config-file - /app/config/tensorzero.toml environment: - - TENSORZERO_CLICKHOUSE_URL=${TENSORZERO_CLICKHOUSE_URL:-http://tensorzero:tensorzero@tensorzero-clickhouse:8123/default} + - TENSORZERO_CLICKHOUSE_URL=${TENSORZERO_CLICKHOUSE_GATEWAY_URL:-${TENSORZERO_CLICKHOUSE_URL:-http://tensorzero:tensorzero@tensorzero-clickhouse:8123/default}} + # Keep gateway bootable in local-first mode when cloud provider keys are absent. + - OPENAI_API_KEY=${OPENAI_API_KEY:-local-disabled} + - GROQ_API_KEY=${GROQ_API_KEY:-local-disabled} + - MOONSHOT_API_KEY=${MOONSHOT_API_KEY:-local-disabled} + - OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-local-disabled} + - VENICE_API_KEY=${VENICE_API_KEY:-local-disabled} + - Z_AI_API_KEY=${Z_AI_API_KEY:-local-disabled} + - TOGETHER_AI_API_KEY=${TOGETHER_AI_API_KEY:-local-disabled} + - CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN:-local-disabled} + - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:-http://host.docker.internal:4317} - DOCKED_MODE=${DOCKED_MODE:-true} - PARENT_SYSTEM=${PARENT_SYSTEM:-PMOVES.AI} - PARENT_VERSION=${PARENT_VERSION:-1.0.0-hardened} @@ -1904,6 +1915,7 @@ services: - pmoves_api - pmoves_bus - pmoves_data + - pmoves_external extra_hosts: - host.docker.internal:host-gateway tensorzero-ui: @@ -1913,7 +1925,7 @@ services: restart: unless-stopped environment: - TENSORZERO_GATEWAY_URL=${TENSORZERO_GATEWAY_URL:-http://tensorzero-gateway:3000} - - TENSORZERO_CLICKHOUSE_URL=${TENSORZERO_CLICKHOUSE_URL:-http://tensorzero:tensorzero@tensorzero-clickhouse:8123/default} + - TENSORZERO_CLICKHOUSE_URL=${TENSORZERO_CLICKHOUSE_GATEWAY_URL:-${TENSORZERO_CLICKHOUSE_URL:-http://tensorzero:tensorzero@tensorzero-clickhouse:8123/default}} - DOCKED_MODE=${DOCKED_MODE:-true} - PARENT_SYSTEM=${PARENT_SYSTEM:-PMOVES.AI} - PARENT_VERSION=${PARENT_VERSION:-1.0.0-hardened} @@ -1931,6 +1943,7 @@ services: networks: - pmoves_app - pmoves_data + - pmoves_external gpu-orchestrator: env_file: - env.shared diff --git a/pmoves/env.jellyfin-ai.example b/pmoves/env.jellyfin-ai.example index 83db7a6b93..0ae4998e5e 100644 --- a/pmoves/env.jellyfin-ai.example +++ b/pmoves/env.jellyfin-ai.example @@ -4,6 +4,14 @@ SUPABASE_URL=https://supabase.cataclysmstudios.net SUPABASE_ANON_KEY=anon-key SUPABASE_SERVICE_ROLE_KEY=service-role-key +SUPABASE_SERVICE_KEY=service-role-key +SUPABASE_JWT_SECRET=${JWT_SECRET} +AUTH_BOOTSTRAP_MODE=jwt + +# Core mesh auth + bus (must align with pmoves/env.shared in production) +NATS_URL=nats://nats:pmoves@nats:4222 +TENSORZERO_URL=http://tensorzero-gateway:3000 +GPU_ORCHESTRATOR_URL=http://gpu-orchestrator:8200 # Jellyfin credentials (username/password OR API key) JELLYFIN_URL=http://jellyfin:8096 diff --git a/pmoves/env.shared.example b/pmoves/env.shared.example index 326a17eb44..c7ebc0b564 100644 --- a/pmoves/env.shared.example +++ b/pmoves/env.shared.example @@ -37,6 +37,7 @@ TENSORZERO_EMBED_BATCH_SIZE=16 TENSORZERO_EMBED_TIMEOUT_SECS=120 # TensorZero ClickHouse - WARNING: Change credentials for production TENSORZERO_CLICKHOUSE_URL=http://tensorzero-clickhouse:8123 +TENSORZERO_CLICKHOUSE_GATEWAY_URL=http://tensorzero:tensorzero@tensorzero-clickhouse:8123/default TENSORZERO_CLICKHOUSE_USER=CLICKHOUSE_USER_HERE TENSORZERO_CLICKHOUSE_PASSWORD=CLICKHOUSE_PASSWORD_HERE TENSORZERO_CLICKHOUSE_DB=tensorzero diff --git a/pmoves/services/jellyfin-bridge/Dockerfile b/pmoves/services/jellyfin-bridge/Dockerfile index eb89a37164..9eeb99c700 100644 --- a/pmoves/services/jellyfin-bridge/Dockerfile +++ b/pmoves/services/jellyfin-bridge/Dockerfile @@ -2,8 +2,8 @@ FROM python:3.11-slim WORKDIR /app ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 RUN pip install --no-cache-dir --upgrade pip -COPY requirements.txt requirements.lock ./ -RUN pip install --no-cache-dir --constraint requirements.lock -r requirements.txt +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt COPY . . # Security: Run as non-root user diff --git a/pmoves/services/jellyfin-bridge/requirements.txt b/pmoves/services/jellyfin-bridge/requirements.txt index 5e3cc9bf23..4ff204a012 100644 --- a/pmoves/services/jellyfin-bridge/requirements.txt +++ b/pmoves/services/jellyfin-bridge/requirements.txt @@ -1,5 +1,4 @@ -# Prometheus metrics -prometheus-client==0.21.0 \ - --hash=sha256:4fa6b4dd0ac16d58bb587c04b1caae65b8c5043e85f778f42f5f632f6af2e166 - --r requirements.lock +fastapi==0.121.0 +uvicorn[standard]==0.38.0 +httpx==0.28.1 +prometheus-client==0.21.0 diff --git a/pmoves/services/pmoves-yt/yt.py b/pmoves/services/pmoves-yt/yt.py index 6effeb6c10..b21599683f 100644 --- a/pmoves/services/pmoves-yt/yt.py +++ b/pmoves/services/pmoves-yt/yt.py @@ -1685,10 +1685,29 @@ def yt_info(body: Dict[str,Any] = Body(...)): url = body.get('url') if not url: raise HTTPException(400, 'url required') ydl_opts = _with_ytdlp_defaults({'quiet': True, 'noprogress': True, 'skip_download': True}) - with yt_dlp.YoutubeDL(ydl_opts) as ydl: - info = ydl.extract_info(url, download=False) - wanted = {k: info.get(k) for k in ('id','title','uploader','duration','webpage_url')} - return {'ok': True, 'info': wanted} + # Metadata probes must not force a playable/download format because + # upstream extractor availability can vary and cause false 500s. + ydl_opts.pop('format', None) + ydl_opts.pop('merge_output_format', None) + ydl_opts.setdefault('extract_flat', True) + # Ignore external yt-dlp config files to keep API behavior deterministic. + ydl_opts['ignoreconfig'] = True + try: + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + info = ydl.extract_info(url, download=False) + except Exception: + # Conservative fallback that avoids hardened defaults entirely. + fallback_opts = { + 'quiet': True, + 'noprogress': True, + 'skip_download': True, + 'extract_flat': True, + 'ignoreconfig': True, + } + with yt_dlp.YoutubeDL(fallback_opts) as ydl: + info = ydl.extract_info(url, download=False) + wanted = {k: info.get(k) for k in ('id','title','uploader','duration','webpage_url')} + return {'ok': True, 'info': wanted} @app.post("/yt/download") def yt_download(body: Dict[str,Any] = Body(...)):