feat: Update Extraction model and service to support deferred service detection

This commit is contained in:
JSC
2025-07-29 10:50:50 +02:00
parent 9b5f83eef0
commit e3fcab99ae
4 changed files with 227 additions and 176 deletions

View File

@@ -51,7 +51,8 @@ class TestExtractionService:
assert result == expected
@patch("app.services.extraction.yt_dlp.YoutubeDL")
def test_detect_service_info_youtube(self, mock_ydl_class, extraction_service):
@pytest.mark.asyncio
async def test_detect_service_info_youtube(self, mock_ydl_class, extraction_service):
"""Test service detection for YouTube."""
mock_ydl = Mock()
mock_ydl_class.return_value.__enter__.return_value = mock_ydl
@@ -63,7 +64,7 @@ class TestExtractionService:
"uploader": "Test Channel",
}
result = extraction_service._detect_service_info(
result = await extraction_service._detect_service_info(
"https://www.youtube.com/watch?v=test123"
)
@@ -71,16 +72,16 @@ class TestExtractionService:
assert result["service"] == "youtube"
assert result["service_id"] == "test123"
assert result["title"] == "Test Video"
assert result["duration"] == 240
@patch("app.services.extraction.yt_dlp.YoutubeDL")
def test_detect_service_info_failure(self, mock_ydl_class, extraction_service):
@pytest.mark.asyncio
async def test_detect_service_info_failure(self, mock_ydl_class, extraction_service):
"""Test service detection failure."""
mock_ydl = Mock()
mock_ydl_class.return_value.__enter__.return_value = mock_ydl
mock_ydl.extract_info.side_effect = Exception("Network error")
result = extraction_service._detect_service_info("https://invalid.url")
result = await extraction_service._detect_service_info("https://invalid.url")
assert result is None
@@ -90,86 +91,140 @@ class TestExtractionService:
url = "https://www.youtube.com/watch?v=test123"
user_id = 1
# Mock service detection
service_info = {
"service": "youtube",
"service_id": "test123",
"title": "Test Video",
}
# Mock repository call - no service detection happens during creation
mock_extraction = Extraction(
id=1,
url=url,
user_id=user_id,
service=None, # Service detection deferred to processing
service_id=None, # Service detection deferred to processing
title=None, # Service detection deferred to processing
status="pending",
)
extraction_service.extraction_repo.create = AsyncMock(
return_value=mock_extraction
)
with patch.object(
extraction_service, "_detect_service_info", return_value=service_info
):
# Mock repository calls
extraction_service.extraction_repo.get_by_service_and_id = AsyncMock(
return_value=None
)
mock_extraction = Extraction(
id=1,
url=url,
user_id=user_id,
service="youtube",
service_id="test123",
title="Test Video",
status="pending",
)
extraction_service.extraction_repo.create = AsyncMock(
return_value=mock_extraction
)
result = await extraction_service.create_extraction(url, user_id)
result = await extraction_service.create_extraction(url, user_id)
assert result["id"] == 1
assert result["service"] == "youtube"
assert result["service_id"] == "test123"
assert result["title"] == "Test Video"
assert result["status"] == "pending"
assert result["id"] == 1
assert result["service"] is None # Not detected during creation
assert result["service_id"] is None # Not detected during creation
assert result["title"] is None # Not detected during creation
assert result["status"] == "pending"
@pytest.mark.asyncio
async def test_create_extraction_duplicate(self, extraction_service):
"""Test extraction creation with duplicate service/service_id."""
async def test_create_extraction_basic(self, extraction_service):
"""Test basic extraction creation without validation."""
url = "https://www.youtube.com/watch?v=test123"
user_id = 1
# Mock service detection
service_info = {
"service": "youtube",
"service_id": "test123",
"title": "Test Video",
}
existing_extraction = Extraction(
id=1,
# Mock repository call - creation always succeeds now
mock_extraction = Extraction(
id=2,
url=url,
user_id=2, # Different user
service="youtube",
service_id="test123",
status="completed",
user_id=user_id,
service=None,
service_id=None,
title=None,
status="pending",
)
extraction_service.extraction_repo.create = AsyncMock(
return_value=mock_extraction
)
with patch.object(
extraction_service, "_detect_service_info", return_value=service_info
):
extraction_service.extraction_repo.get_by_service_and_id = AsyncMock(
return_value=existing_extraction
)
result = await extraction_service.create_extraction(url, user_id)
with pytest.raises(ValueError, match="Extraction already exists"):
await extraction_service.create_extraction(url, user_id)
assert result["id"] == 2
assert result["url"] == url
assert result["status"] == "pending"
@pytest.mark.asyncio
async def test_create_extraction_invalid_url(self, extraction_service):
"""Test extraction creation with invalid URL."""
async def test_create_extraction_any_url(self, extraction_service):
"""Test extraction creation accepts any URL."""
url = "https://invalid.url"
user_id = 1
with patch.object(
extraction_service, "_detect_service_info", return_value=None
# Mock repository call - even invalid URLs are accepted during creation
mock_extraction = Extraction(
id=3,
url=url,
user_id=user_id,
service=None,
service_id=None,
title=None,
status="pending",
)
extraction_service.extraction_repo.create = AsyncMock(
return_value=mock_extraction
)
result = await extraction_service.create_extraction(url, user_id)
assert result["id"] == 3
assert result["url"] == url
assert result["status"] == "pending"
@pytest.mark.asyncio
async def test_process_extraction_with_service_detection(self, extraction_service):
"""Test extraction processing with service detection."""
extraction_id = 1
# Mock extraction without service info
mock_extraction = Extraction(
id=extraction_id,
url="https://www.youtube.com/watch?v=test123",
user_id=1,
service=None,
service_id=None,
title=None,
status="pending",
)
extraction_service.extraction_repo.get_by_id = AsyncMock(
return_value=mock_extraction
)
extraction_service.extraction_repo.update = AsyncMock()
extraction_service.extraction_repo.get_by_service_and_id = AsyncMock(
return_value=None
)
# Mock service detection
service_info = {
"service": "youtube",
"service_id": "test123",
"title": "Test Video",
}
with (
patch.object(
extraction_service, "_detect_service_info", return_value=service_info
),
patch.object(extraction_service, "_extract_media") as mock_extract,
patch.object(extraction_service, "_move_files_to_final_location") as mock_move,
patch.object(extraction_service, "_create_sound_record") as mock_create_sound,
patch.object(extraction_service, "_normalize_sound") as mock_normalize,
patch.object(extraction_service, "_add_to_main_playlist") as mock_playlist,
):
with pytest.raises(
ValueError, match="Unable to detect service information"
):
await extraction_service.create_extraction(url, user_id)
mock_sound = Sound(id=42, type="EXT", name="Test", filename="test.mp3")
mock_extract.return_value = (Path("/fake/audio.mp3"), None)
mock_move.return_value = (Path("/final/audio.mp3"), None)
mock_create_sound.return_value = mock_sound
result = await extraction_service.process_extraction(extraction_id)
# Verify service detection was called
extraction_service._detect_service_info.assert_called_once_with(
"https://www.youtube.com/watch?v=test123"
)
# Verify extraction was updated with service info
extraction_service.extraction_repo.update.assert_called()
assert result["status"] == "completed"
assert result["service"] == "youtube"
assert result["service_id"] == "test123"
assert result["title"] == "Test Video"
def test_ensure_unique_filename(self, extraction_service):
"""Test unique filename generation."""