From 067cc51cf241871869a21268bbd0ac8f0dfc8ee8 Mon Sep 17 00:00:00 2001 From: pie Date: Sun, 3 May 2026 17:19:26 +0100 Subject: [PATCH] feat: resolve chromecast discovery conflict and add CI/CD workflow - Update Zeroconf to unicast mode to resolve port 5353 conflict with avahi-daemon - Make API and Streamlit ports configurable via environment variables (defaults: 8055, 8505) - Add Gitea Actions workflow for automated Docker builds and registry pushes - Refactor Chromecast discovery to use modern CastBrowser API --- .gitea/workflows/docker-build.yml | 33 +++++++++++++++++++++ Dockerfile | 8 ++++-- app/api.py | 7 ++--- app/cast_logic.py | 48 +++++++++++++++++++++++++------ app/main.py | 4 +-- app/utils.py | 4 +++ docker-compose.yml | 4 ++- start.sh | 8 +++--- 8 files changed, 95 insertions(+), 21 deletions(-) create mode 100644 .gitea/workflows/docker-build.yml diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml new file mode 100644 index 0000000..003a322 --- /dev/null +++ b/.gitea/workflows/docker-build.yml @@ -0,0 +1,33 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - main + +env: + REGISTRY: git.home.piesweb.co.uk + IMAGE_NAME: pie/boys_streaming + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Docker image + run: | + docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \ + -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} . + + - name: Push Docker image + run: | + docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} --all-tags diff --git a/Dockerfile b/Dockerfile index 1cefc87..f8b8dc7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,8 +20,12 @@ RUN chmod +x start.sh # Create necessary directories RUN mkdir -p config/thumbnails videos -# Expose ports (FastAPI: 8003, Streamlit: 8503) -EXPOSE 8003 8503 +# Default ports +ENV API_PORT=8055 +ENV STREAMLIT_PORT=8505 + +# Expose ports +EXPOSE 8055 8505 # Set Environment Variables ENV CONFIG_DIR=/app/config diff --git a/app/api.py b/app/api.py index 071f2a3..cfb8834 100644 --- a/app/api.py +++ b/app/api.py @@ -5,7 +5,7 @@ from typing import List import os from .library import scan_library from .cast_logic import cast_manager -from .utils import get_logger, VIDEOS_DIR, THUMBNAILS_DIR, get_host_ip +from .utils import get_logger, VIDEOS_DIR, THUMBNAILS_DIR, get_host_ip, API_PORT logger = get_logger(__name__) @@ -25,9 +25,8 @@ async def get_library(): @app.post("/cast") async def start_casting(request: CastRequest, background_tasks: BackgroundTasks): host_ip = get_host_ip() - # Port is 8000 by default for uvicorn in our setup - urls = [f"http://{host_ip}:8000/media/{path}" for path in request.video_paths] - + urls = [f"http://{host_ip}:{API_PORT}/media/{path}" for path in request.video_paths] + logger.info(f"Received cast request for {len(urls)} videos") cast_manager.play_queue(urls) return {"status": "started", "queue_len": len(urls)} diff --git a/app/cast_logic.py b/app/cast_logic.py index 485b5d5..acd4cd2 100644 --- a/app/cast_logic.py +++ b/app/cast_logic.py @@ -17,16 +17,48 @@ class CastManager: self._lock = threading.Lock() self._discover_thread = None self.browser = None + self.zconf = None + self.found_devices = {} def discover(self, timeout=5): - logger.info("Discovering Chromecasts...") - chromecasts, browser = pychromecast.get_chromecasts(timeout=timeout) - self.browser = browser - if chromecasts: - # For simplicity, pick the first one found or allow selection later - # In a real scenario, we might want to target a specific name - self.chromecast = chromecasts[0] - logger.info(f"Found Chromecast: {self.chromecast.name}") + logger.info("Starting Chromecast discovery...") + import zeroconf + from pychromecast.discovery import CastBrowser, SimpleCastListener + from .utils import get_host_ip + + if not self.browser: + try: + ip = get_host_ip() + logger.info(f"Binding zeroconf to IP: {ip} (unicast mode)") + # unicast=True prevents binding to port 5353, avoiding conflict with Avahi + self.zconf = zeroconf.Zeroconf(interfaces=[ip], unicast=True) + except Exception as e: + logger.error(f"Zeroconf init failed: {e}") + self.zconf = zeroconf.Zeroconf(unicast=True) + + self.found_devices = {} + + def add_callback(uuid, service): + cast_info = self.browser.devices[uuid] + cast = pychromecast.get_chromecast_from_cast_info(cast_info, self.zconf) + self.found_devices[cast.name] = cast + logger.info(f"Discovered Chromecast: {cast.name}") + + listener = SimpleCastListener(add_callback=add_callback) + self.browser = CastBrowser(listener, zeroconf_instance=self.zconf) + self.browser.start_discovery() + + # Wait up to timeout seconds to find at least one + start_time = time.time() + while time.time() - start_time < timeout: + if self.found_devices: + break + time.sleep(0.1) + + if self.found_devices: + # Pick the first one + self.chromecast = list(self.found_devices.values())[0] + logger.info(f"Selected Chromecast: {self.chromecast.name}") self.chromecast.wait() self.mc = self.chromecast.media_controller self.mc.register_status_listener(self) diff --git a/app/main.py b/app/main.py index 53bc99f..bf1bacf 100644 --- a/app/main.py +++ b/app/main.py @@ -2,13 +2,13 @@ import streamlit as st import httpx import time from pathlib import Path -from utils import get_logger, get_host_ip +from utils import get_logger, get_host_ip, API_PORT logger = get_logger(__name__) # Config - Use host IP so the browser can find the API HOST_IP = get_host_ip() -API_URL = f"http://{HOST_IP}:8000" +API_URL = f"http://{HOST_IP}:{API_PORT}" st.set_page_config( page_title="Boys Streaming 📺", diff --git a/app/utils.py b/app/utils.py index 97750f5..7a8e8ad 100644 --- a/app/utils.py +++ b/app/utils.py @@ -16,6 +16,10 @@ CONFIG_DIR.mkdir(parents=True, exist_ok=True) THUMBNAILS_DIR.mkdir(parents=True, exist_ok=True) VIDEOS_DIR.mkdir(parents=True, exist_ok=True) +# Port configuration +API_PORT = int(os.getenv("API_PORT", "8055")) +STREAMLIT_PORT = int(os.getenv("STREAMLIT_PORT", "8505")) + # Logging configuration logging.basicConfig( level=logging.INFO, diff --git a/docker-compose.yml b/docker-compose.yml index 48b59f8..c79f102 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,8 +12,10 @@ services: environment: - VIDEOS_DIR=/app/videos - CONFIG_DIR=/app/config + - API_PORT=8055 + - STREAMLIT_PORT=8505 healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8501/_stcore/health"] + test: ["CMD", "curl", "-f", "http://localhost:8505/_stcore/health"] interval: 30s timeout: 10s retries: 3 diff --git a/start.sh b/start.sh index 51d5ccc..c7972bb 100644 --- a/start.sh +++ b/start.sh @@ -1,12 +1,12 @@ #!/bin/bash # Start FastAPI backend in the background -echo "Starting FastAPI backend..." -uvicorn app.api:app --host 0.0.0.0 --port 8003 & +echo "Starting FastAPI backend on port ${API_PORT:-8055}..." +uvicorn app.api:app --host 0.0.0.0 --port ${API_PORT:-8055} & # Start Streamlit frontend -echo "Starting Streamlit frontend..." -streamlit run app/main.py --server.port 8503 --server.address 0.0.0.0 +echo "Starting Streamlit frontend on port ${STREAMLIT_PORT:-8505}..." +streamlit run app/main.py --server.port ${STREAMLIT_PORT:-8505} --server.address 0.0.0.0 # Wait for any process to exit wait -n