feat: resolve chromecast discovery conflict and add CI/CD workflow
Build and Push Docker Image / build-and-push (push) Failing after 10m6s
Build and Push Docker Image / build-and-push (push) Failing after 10m6s
- 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
This commit is contained in:
@@ -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
|
||||||
+6
-2
@@ -20,8 +20,12 @@ RUN chmod +x start.sh
|
|||||||
# Create necessary directories
|
# Create necessary directories
|
||||||
RUN mkdir -p config/thumbnails videos
|
RUN mkdir -p config/thumbnails videos
|
||||||
|
|
||||||
# Expose ports (FastAPI: 8003, Streamlit: 8503)
|
# Default ports
|
||||||
EXPOSE 8003 8503
|
ENV API_PORT=8055
|
||||||
|
ENV STREAMLIT_PORT=8505
|
||||||
|
|
||||||
|
# Expose ports
|
||||||
|
EXPOSE 8055 8505
|
||||||
|
|
||||||
# Set Environment Variables
|
# Set Environment Variables
|
||||||
ENV CONFIG_DIR=/app/config
|
ENV CONFIG_DIR=/app/config
|
||||||
|
|||||||
+3
-4
@@ -5,7 +5,7 @@ from typing import List
|
|||||||
import os
|
import os
|
||||||
from .library import scan_library
|
from .library import scan_library
|
||||||
from .cast_logic import cast_manager
|
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__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -25,9 +25,8 @@ async def get_library():
|
|||||||
@app.post("/cast")
|
@app.post("/cast")
|
||||||
async def start_casting(request: CastRequest, background_tasks: BackgroundTasks):
|
async def start_casting(request: CastRequest, background_tasks: BackgroundTasks):
|
||||||
host_ip = get_host_ip()
|
host_ip = get_host_ip()
|
||||||
# Port is 8000 by default for uvicorn in our setup
|
urls = [f"http://{host_ip}:{API_PORT}/media/{path}" for path in request.video_paths]
|
||||||
urls = [f"http://{host_ip}:8000/media/{path}" for path in request.video_paths]
|
|
||||||
|
|
||||||
logger.info(f"Received cast request for {len(urls)} videos")
|
logger.info(f"Received cast request for {len(urls)} videos")
|
||||||
cast_manager.play_queue(urls)
|
cast_manager.play_queue(urls)
|
||||||
return {"status": "started", "queue_len": len(urls)}
|
return {"status": "started", "queue_len": len(urls)}
|
||||||
|
|||||||
+40
-8
@@ -17,16 +17,48 @@ class CastManager:
|
|||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
self._discover_thread = None
|
self._discover_thread = None
|
||||||
self.browser = None
|
self.browser = None
|
||||||
|
self.zconf = None
|
||||||
|
self.found_devices = {}
|
||||||
|
|
||||||
def discover(self, timeout=5):
|
def discover(self, timeout=5):
|
||||||
logger.info("Discovering Chromecasts...")
|
logger.info("Starting Chromecast discovery...")
|
||||||
chromecasts, browser = pychromecast.get_chromecasts(timeout=timeout)
|
import zeroconf
|
||||||
self.browser = browser
|
from pychromecast.discovery import CastBrowser, SimpleCastListener
|
||||||
if chromecasts:
|
from .utils import get_host_ip
|
||||||
# For simplicity, pick the first one found or allow selection later
|
|
||||||
# In a real scenario, we might want to target a specific name
|
if not self.browser:
|
||||||
self.chromecast = chromecasts[0]
|
try:
|
||||||
logger.info(f"Found Chromecast: {self.chromecast.name}")
|
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.chromecast.wait()
|
||||||
self.mc = self.chromecast.media_controller
|
self.mc = self.chromecast.media_controller
|
||||||
self.mc.register_status_listener(self)
|
self.mc.register_status_listener(self)
|
||||||
|
|||||||
+2
-2
@@ -2,13 +2,13 @@ import streamlit as st
|
|||||||
import httpx
|
import httpx
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
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__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
# Config - Use host IP so the browser can find the API
|
# Config - Use host IP so the browser can find the API
|
||||||
HOST_IP = get_host_ip()
|
HOST_IP = get_host_ip()
|
||||||
API_URL = f"http://{HOST_IP}:8000"
|
API_URL = f"http://{HOST_IP}:{API_PORT}"
|
||||||
|
|
||||||
st.set_page_config(
|
st.set_page_config(
|
||||||
page_title="Boys Streaming 📺",
|
page_title="Boys Streaming 📺",
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|||||||
THUMBNAILS_DIR.mkdir(parents=True, exist_ok=True)
|
THUMBNAILS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
VIDEOS_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 configuration
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
|
|||||||
+3
-1
@@ -12,8 +12,10 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- VIDEOS_DIR=/app/videos
|
- VIDEOS_DIR=/app/videos
|
||||||
- CONFIG_DIR=/app/config
|
- CONFIG_DIR=/app/config
|
||||||
|
- API_PORT=8055
|
||||||
|
- STREAMLIT_PORT=8505
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8501/_stcore/health"]
|
test: ["CMD", "curl", "-f", "http://localhost:8505/_stcore/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Start FastAPI backend in the background
|
# Start FastAPI backend in the background
|
||||||
echo "Starting FastAPI backend..."
|
echo "Starting FastAPI backend on port ${API_PORT:-8055}..."
|
||||||
uvicorn app.api:app --host 0.0.0.0 --port 8003 &
|
uvicorn app.api:app --host 0.0.0.0 --port ${API_PORT:-8055} &
|
||||||
|
|
||||||
# Start Streamlit frontend
|
# Start Streamlit frontend
|
||||||
echo "Starting Streamlit frontend..."
|
echo "Starting Streamlit frontend on port ${STREAMLIT_PORT:-8505}..."
|
||||||
streamlit run app/main.py --server.port 8503 --server.address 0.0.0.0
|
streamlit run app/main.py --server.port ${STREAMLIT_PORT:-8505} --server.address 0.0.0.0
|
||||||
|
|
||||||
# Wait for any process to exit
|
# Wait for any process to exit
|
||||||
wait -n
|
wait -n
|
||||||
|
|||||||
Reference in New Issue
Block a user