Add ffmpeg for HLS stream download support

- Add ffmpeg to Dockerfile.app for container HLS support
- Configure yt-dlp with --downloader ffmpeg --hls-use-mpegts
- Add startup health check warns if ffmpeg missing
- Update DEVELOPMENT.md with ffmpeg prerequisites and troubleshooting
- Add tests for ffmpeg HLS options and health check
This commit is contained in:
2026-05-23 21:18:39 +02:00
parent db65e28854
commit e74b602f60
5 changed files with 116 additions and 2 deletions

View File

@@ -917,4 +917,47 @@ class TestAniworldLoaderCompat:
"""AniworldLoader should extend EnhancedAniWorldLoader."""
from src.core.providers.enhanced_provider import AniworldLoader
assert issubclass(AniworldLoader, EnhancedAniWorldLoader)
assert issubclass(AniworldLoader, EnhancedAniWorldLoader)
class TestFfmpegHlsOptions:
"""Test that yt-dlp is configured with ffmpeg for HLS streams."""
def test_ytdl_opts_include_ffmpeg_for_hls(self, enhanced_loader, tmp_path):
"""yt-dlp options should include ffmpeg downloader and hls-use-mpegts."""
temp_path = str(tmp_path / "temp.mp4")
output_path = str(tmp_path / "output.mp4")
captured_opts = {}
def capture_ytdl_download(ydl_opts, link):
captured_opts.update(ydl_opts)
with open(temp_path, "wb") as f:
f.write(b"fake-video-data")
return True
with patch(
"src.core.providers.enhanced_provider.recovery_strategies"
) as mock_rs, patch(
"src.core.providers.enhanced_provider.file_corruption_detector"
) as mock_fcd, patch(
"src.core.providers.enhanced_provider.get_integrity_manager"
) as mock_im:
mock_rs.handle_network_failure.return_value = (
"https://direct.example.com/v.mp4",
[],
)
mock_rs.handle_download_failure.side_effect = capture_ytdl_download
mock_fcd.is_valid_video_file.return_value = True
mock_im.return_value.store_checksum.return_value = "abc123"
enhanced_loader._download_with_recovery(
1, 1, "test", "German Dub",
temp_path, output_path, None,
)
assert captured_opts.get("downloader") == "ffmpeg", (
f"Expected downloader='ffmpeg', got {captured_opts.get('downloader')}"
)
assert captured_opts.get("hls_use_mpegts") is True, (
f"Expected hls_use_mpegts=True, got {captured_opts.get('hls_use_mpegts')}"
)

View File

@@ -0,0 +1,54 @@
"""Unit tests for ffmpeg health check in fastapi_app.py."""
import asyncio
from unittest.mock import MagicMock, patch
import pytest
class TestFfmpegHealthCheck:
"""Test ffmpeg health check warns when not in PATH."""
@pytest.mark.asyncio
async def test_ffmpeg_missing_warns(self):
"""Should log warning when ffmpeg not found in PATH."""
with patch("shutil.which", return_value=None):
with patch("src.server.fastapi_app.setup_logging") as mock_log:
mock_logger = MagicMock()
mock_log.return_value = mock_logger
from src.server.fastapi_app import lifespan
app = MagicMock()
with pytest.raises(StopIteration):
async with lifespan(app):
pass
# Should have logged a warning about ffmpeg
warning_calls = [
c for c in mock_logger.warning.call_args_list
if "ffmpeg" in str(c)
]
assert len(warning_calls) >= 1
@pytest.mark.asyncio
async def test_ffmpeg_present_no_warning(self):
"""Should not log warning when ffmpeg is found."""
with patch("shutil.which", return_value="/usr/bin/ffmpeg"):
with patch("src.server.fastapi_app.setup_logging") as mock_log:
mock_logger = MagicMock()
mock_log.return_value = mock_logger
from src.server.fastapi_app import lifespan
app = MagicMock()
with pytest.raises(StopIteration):
async with lifespan(app):
pass
# Should NOT have logged a warning about ffmpeg
warning_calls = [
c for c in mock_logger.warning.call_args_list
if "ffmpeg" in str(c)
]
assert len(warning_calls) == 0