Aniworld/instructions.md

1003 lines
33 KiB
Markdown

# Aniworld Web Application Development Instructions
This document provides detailed tasks for AI agents to implement a modern web application for the Aniworld anime download manager. All tasks should follow the coding guidelines specified in the project's copilot instructions.
## Project Overview
The goal is to create a FastAPI-based web application that provides a modern interface for the existing Aniworld anime download functionality. The core anime logic should remain in `SeriesApp.py` while the web layer provides REST API endpoints and a responsive UI.
## Architecture Principles
- **Single Responsibility**: Each file/class has one clear purpose
- **Dependency Injection**: Use FastAPI's dependency system
- **Clean Separation**: Web layer calls core logic, never the reverse
- **File Size Limit**: Maximum 500 lines per file
- **Type Hints**: Use comprehensive type annotations
- **Error Handling**: Proper exception handling and logging
## Additional Implementation Guidelines
### Code Style and Standards
- **Type Hints**: Use comprehensive type annotations throughout all modules
- **Docstrings**: Follow PEP 257 for function and class documentation
- **Error Handling**: Implement custom exception classes with meaningful messages
- **Logging**: Use structured logging with appropriate log levels
- **Security**: Validate all inputs and sanitize outputs
- **Performance**: Use async/await patterns for I/O operations
## 📞 Escalation
If you encounter:
- Architecture issues requiring design decisions
- Tests that conflict with documented requirements
- Breaking changes needed
- Unclear requirements or expectations
**Document the issue and escalate rather than guessing.**
---
## 📚 Helpful Commands
```bash
# Run all tests
conda run -n AniWorld python -m pytest tests/ -v --tb=short
# Run specific test file
conda run -n AniWorld python -m pytest tests/unit/test_websocket_service.py -v
# Run specific test class
conda run -n AniWorld python -m pytest tests/unit/test_websocket_service.py::TestWebSocketService -v
# Run specific test
conda run -n AniWorld python -m pytest tests/unit/test_websocket_service.py::TestWebSocketService::test_broadcast_download_progress -v
# Run with extra verbosity
conda run -n AniWorld python -m pytest tests/ -vv
# Run with full traceback
conda run -n AniWorld python -m pytest tests/ -v --tb=long
# Run and stop at first failure
conda run -n AniWorld python -m pytest tests/ -v -x
# Run tests matching pattern
conda run -n AniWorld python -m pytest tests/ -v -k "auth"
# Show all print statements
conda run -n AniWorld python -m pytest tests/ -v -s
#Run app
conda run -n AniWorld python -m uvicorn src.server.fastapi_app:app --host 127.0.0.1 --port 8000 --reload
```
---
## Implementation Notes
1. **Incremental Development**: Implement features incrementally, testing each component thoroughly before moving to the next
2. **Code Review**: Review all generated code for adherence to project standards
3. **Documentation**: Document all public APIs and complex logic
4. **Testing**: Maintain test coverage above 80% for all new code
5. **Performance**: Profile and optimize critical paths, especially download and streaming operations
6. **Security**: Regular security audits and dependency updates
7. **Monitoring**: Implement comprehensive monitoring and alerting
8. **Maintenance**: Plan for regular maintenance and updates
## Task Completion Checklist
For each task completed:
- [ ] Implementation follows coding standards
- [ ] Unit tests written and passing
- [ ] Integration tests passing
- [ ] Documentation updated
- [ ] Error handling implemented
- [ ] Logging added
- [ ] Security considerations addressed
- [ ] Performance validated
- [ ] Code reviewed
- [ ] Task marked as complete in instructions.md
- [ ] Infrastructure.md updated
- [ ] Changes committed to git; keep your messages in git short and clear
- [ ] Take the next task
---
### Prerequisites
1. Server is running: `conda run -n AniWorld python -m uvicorn src.server.fastapi_app:app --host 127.0.0.1 --port 8000 --reload`
2. Password: `Hallo123!`
3. Login via browser at `http://127.0.0.1:8000/login`
### Notes
- This is a simplification that removes complexity while maintaining core functionality
- Improves user experience with explicit manual control
- Easier to understand, test, and maintain
- Good foundation for future enhancements if needed
---
## ✅ Completed: Download Queue Migration to SQLite Database
The download queue has been successfully migrated from JSON file to SQLite database:
| Component | Status | Description |
| --------------------- | ------- | ------------------------------------------------- |
| QueueRepository | ✅ Done | `src/server/services/queue_repository.py` |
| DownloadService | ✅ Done | Refactored to use repository pattern |
| Application Startup | ✅ Done | Queue restored from database on startup |
| API Endpoints | ✅ Done | All endpoints work with database-backed queue |
| Tests Updated | ✅ Done | All 1104 tests passing with MockQueueRepository |
| Documentation Updated | ✅ Done | `infrastructure.md` updated with new architecture |
**Key Changes:**
- `DownloadService` no longer uses `persistence_path` parameter
- Queue state is persisted to SQLite via `QueueRepository`
- In-memory cache maintained for performance
- All tests use `MockQueueRepository` fixture
---
## 🧪 Tests for Download Queue Database Migration
### Unit Tests
**File:** `tests/unit/test_queue_repository.py`
```python
"""Unit tests for QueueRepository database adapter."""
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from datetime import datetime, timezone
from src.server.services.queue_repository import QueueRepository
from src.server.models.download import DownloadItem, DownloadStatus, DownloadPriority
from src.server.database.models import DownloadQueueItem as DBDownloadQueueItem
class TestQueueRepository:
"""Test suite for QueueRepository."""
@pytest.fixture
def mock_db_session(self):
"""Create mock database session."""
session = AsyncMock()
return session
@pytest.fixture
def repository(self, mock_db_session):
"""Create repository instance with mock session."""
return QueueRepository(db_session_factory=lambda: mock_db_session)
@pytest.fixture
def sample_download_item(self):
"""Create sample DownloadItem for testing."""
return DownloadItem(
id="test-uuid-123",
series_key="attack-on-titan",
series_name="Attack on Titan",
season=1,
episode=5,
status=DownloadStatus.PENDING,
priority=DownloadPriority.NORMAL,
progress_percent=0.0,
downloaded_bytes=0,
total_bytes=None,
)
# === Conversion Tests ===
async def test_convert_to_db_model(self, repository, sample_download_item):
"""Test converting DownloadItem to database model."""
# Arrange
series_id = 42
# Act
db_item = repository._to_db_model(sample_download_item, series_id)
# Assert
assert db_item.series_id == series_id
assert db_item.season == sample_download_item.season
assert db_item.episode_number == sample_download_item.episode
assert db_item.status == sample_download_item.status
assert db_item.priority == sample_download_item.priority
async def test_convert_from_db_model(self, repository):
"""Test converting database model to DownloadItem."""
# Arrange
db_item = MagicMock()
db_item.id = 1
db_item.series_id = 42
db_item.series.key = "attack-on-titan"
db_item.series.name = "Attack on Titan"
db_item.season = 1
db_item.episode_number = 5
db_item.status = DownloadStatus.PENDING
db_item.priority = DownloadPriority.NORMAL
db_item.progress_percent = 25.5
db_item.downloaded_bytes = 1024000
db_item.total_bytes = 4096000
# Act
item = repository._from_db_model(db_item)
# Assert
assert item.series_key == "attack-on-titan"
assert item.series_name == "Attack on Titan"
assert item.season == 1
assert item.episode == 5
assert item.progress_percent == 25.5
# === CRUD Operation Tests ===
async def test_save_item_creates_new_record(self, repository, mock_db_session, sample_download_item):
"""Test saving a new download item to database."""
# Arrange
mock_db_session.execute.return_value.scalar_one_or_none.return_value = MagicMock(id=42)
# Act
result = await repository.save_item(sample_download_item)
# Assert
mock_db_session.add.assert_called_once()
mock_db_session.flush.assert_called_once()
assert result is not None
async def test_get_pending_items_returns_ordered_list(self, repository, mock_db_session):
"""Test retrieving pending items ordered by priority."""
# Arrange
mock_items = [MagicMock(), MagicMock()]
mock_db_session.execute.return_value.scalars.return_value.all.return_value = mock_items
# Act
result = await repository.get_pending_items()
# Assert
assert len(result) == 2
mock_db_session.execute.assert_called_once()
async def test_update_status_success(self, repository, mock_db_session):
"""Test updating item status."""
# Arrange
mock_item = MagicMock()
mock_db_session.execute.return_value.scalar_one_or_none.return_value = mock_item
# Act
result = await repository.update_status("test-id", DownloadStatus.DOWNLOADING)
# Assert
assert result is True
assert mock_item.status == DownloadStatus.DOWNLOADING
async def test_update_status_item_not_found(self, repository, mock_db_session):
"""Test updating status for non-existent item."""
# Arrange
mock_db_session.execute.return_value.scalar_one_or_none.return_value = None
# Act
result = await repository.update_status("non-existent", DownloadStatus.DOWNLOADING)
# Assert
assert result is False
async def test_update_progress(self, repository, mock_db_session):
"""Test updating download progress."""
# Arrange
mock_item = MagicMock()
mock_db_session.execute.return_value.scalar_one_or_none.return_value = mock_item
# Act
result = await repository.update_progress(
item_id="test-id",
progress=50.0,
downloaded=2048000,
total=4096000,
speed=1024000.0
)
# Assert
assert result is True
assert mock_item.progress_percent == 50.0
assert mock_item.downloaded_bytes == 2048000
async def test_delete_item_success(self, repository, mock_db_session):
"""Test deleting download item."""
# Arrange
mock_db_session.execute.return_value.rowcount = 1
# Act
result = await repository.delete_item("test-id")
# Assert
assert result is True
async def test_clear_completed_returns_count(self, repository, mock_db_session):
"""Test clearing completed items returns count."""
# Arrange
mock_db_session.execute.return_value.rowcount = 5
# Act
result = await repository.clear_completed()
# Assert
assert result == 5
class TestQueueRepositoryErrorHandling:
"""Test error handling in QueueRepository."""
@pytest.fixture
def mock_db_session(self):
"""Create mock database session."""
return AsyncMock()
@pytest.fixture
def repository(self, mock_db_session):
"""Create repository instance."""
return QueueRepository(db_session_factory=lambda: mock_db_session)
async def test_save_item_handles_database_error(self, repository, mock_db_session):
"""Test handling database errors on save."""
# Arrange
mock_db_session.execute.side_effect = Exception("Database connection failed")
# Act & Assert
with pytest.raises(Exception):
await repository.save_item(MagicMock())
async def test_get_items_handles_database_error(self, repository, mock_db_session):
"""Test handling database errors on query."""
# Arrange
mock_db_session.execute.side_effect = Exception("Query failed")
# Act & Assert
with pytest.raises(Exception):
await repository.get_pending_items()
```
---
**File:** `tests/unit/test_download_service_database.py`
```python
"""Unit tests for DownloadService with database persistence."""
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from datetime import datetime, timezone
from src.server.services.download_service import DownloadService
from src.server.models.download import DownloadItem, DownloadStatus, DownloadPriority
class TestDownloadServiceDatabasePersistence:
"""Test DownloadService database persistence."""
@pytest.fixture
def mock_anime_service(self):
"""Create mock anime service."""
return AsyncMock()
@pytest.fixture
def mock_queue_repository(self):
"""Create mock queue repository."""
repo = AsyncMock()
repo.get_pending_items.return_value = []
repo.get_active_item.return_value = None
repo.get_completed_items.return_value = []
repo.get_failed_items.return_value = []
return repo
@pytest.fixture
def download_service(self, mock_anime_service, mock_queue_repository):
"""Create download service with mocked dependencies."""
return DownloadService(
anime_service=mock_anime_service,
queue_repository=mock_queue_repository,
)
# === Persistence Tests ===
async def test_add_to_queue_saves_to_database(
self, download_service, mock_queue_repository
):
"""Test that adding to queue persists to database."""
# Arrange
mock_queue_repository.save_item.return_value = MagicMock(id="new-id")
# Act
result = await download_service.add_to_queue(
series_key="test-series",
season=1,
episode=1,
)
# Assert
mock_queue_repository.save_item.assert_called_once()
async def test_startup_loads_from_database(
self, mock_anime_service, mock_queue_repository
):
"""Test that startup loads queue state from database."""
# Arrange
pending_items = [
MagicMock(id="1", status=DownloadStatus.PENDING),
MagicMock(id="2", status=DownloadStatus.PENDING),
]
mock_queue_repository.get_pending_items.return_value = pending_items
# Act
service = DownloadService(
anime_service=mock_anime_service,
queue_repository=mock_queue_repository,
)
await service.initialize()
# Assert
mock_queue_repository.get_pending_items.assert_called()
async def test_download_completion_updates_database(
self, download_service, mock_queue_repository
):
"""Test that download completion updates database status."""
# Arrange
item = MagicMock(id="test-id")
# Act
await download_service._mark_completed(item)
# Assert
mock_queue_repository.update_status.assert_called_with(
"test-id", DownloadStatus.COMPLETED, error=None
)
async def test_download_failure_updates_database(
self, download_service, mock_queue_repository
):
"""Test that download failure updates database with error."""
# Arrange
item = MagicMock(id="test-id")
error_message = "Network timeout"
# Act
await download_service._mark_failed(item, error_message)
# Assert
mock_queue_repository.update_status.assert_called_with(
"test-id", DownloadStatus.FAILED, error=error_message
)
async def test_progress_update_persists_to_database(
self, download_service, mock_queue_repository
):
"""Test that progress updates are persisted."""
# Arrange
item = MagicMock(id="test-id")
# Act
await download_service._update_progress(
item, progress=50.0, downloaded=2048, total=4096, speed=1024.0
)
# Assert
mock_queue_repository.update_progress.assert_called_with(
item_id="test-id",
progress=50.0,
downloaded=2048,
total=4096,
speed=1024.0,
)
async def test_remove_from_queue_deletes_from_database(
self, download_service, mock_queue_repository
):
"""Test that removing from queue deletes from database."""
# Arrange
mock_queue_repository.delete_item.return_value = True
# Act
result = await download_service.remove_from_queue("test-id")
# Assert
mock_queue_repository.delete_item.assert_called_with("test-id")
assert result is True
async def test_clear_completed_clears_database(
self, download_service, mock_queue_repository
):
"""Test that clearing completed items updates database."""
# Arrange
mock_queue_repository.clear_completed.return_value = 5
# Act
result = await download_service.clear_completed()
# Assert
mock_queue_repository.clear_completed.assert_called_once()
assert result == 5
class TestDownloadServiceNoJsonFile:
"""Verify DownloadService no longer uses JSON files."""
async def test_no_json_file_operations(self):
"""Verify no JSON file read/write operations exist."""
import inspect
from src.server.services.download_service import DownloadService
source = inspect.getsource(DownloadService)
# Assert no JSON file operations
assert "download_queue.json" not in source
assert "_load_queue" not in source or "database" in source.lower()
assert "_save_queue" not in source or "database" in source.lower()
```
---
### Integration Tests
**File:** `tests/integration/test_queue_database_integration.py`
```python
"""Integration tests for download queue database operations."""
import pytest
from datetime import datetime, timezone
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from src.server.database.base import Base
from src.server.database.models import AnimeSeries, DownloadQueueItem, DownloadStatus, DownloadPriority
from src.server.database.service import DownloadQueueService, AnimeSeriesService
from src.server.services.queue_repository import QueueRepository
@pytest.fixture
async def async_engine():
"""Create async test database engine."""
engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield engine
await engine.dispose()
@pytest.fixture
async def async_session(async_engine):
"""Create async session for tests."""
async_session_maker = sessionmaker(
async_engine, class_=AsyncSession, expire_on_commit=False
)
async with async_session_maker() as session:
yield session
await session.rollback()
@pytest.fixture
async def test_series(async_session):
"""Create test anime series."""
series = await AnimeSeriesService.create(
db=async_session,
key="test-anime",
name="Test Anime",
site="https://example.com/test-anime",
folder="Test Anime (2024)",
)
await async_session.commit()
return series
class TestQueueDatabaseIntegration:
"""Integration tests for queue database operations."""
async def test_create_and_retrieve_queue_item(self, async_session, test_series):
"""Test creating and retrieving a queue item."""
# Create
item = await DownloadQueueService.create(
db=async_session,
series_id=test_series.id,
season=1,
episode_number=5,
priority=DownloadPriority.HIGH,
)
await async_session.commit()
# Retrieve
retrieved = await DownloadQueueService.get_by_id(async_session, item.id)
# Assert
assert retrieved is not None
assert retrieved.series_id == test_series.id
assert retrieved.season == 1
assert retrieved.episode_number == 5
assert retrieved.priority == DownloadPriority.HIGH
assert retrieved.status == DownloadStatus.PENDING
async def test_update_download_progress(self, async_session, test_series):
"""Test updating download progress."""
# Create item
item = await DownloadQueueService.create(
db=async_session,
series_id=test_series.id,
season=1,
episode_number=1,
)
await async_session.commit()
# Update progress
updated = await DownloadQueueService.update_progress(
db=async_session,
item_id=item.id,
progress_percent=75.5,
downloaded_bytes=3072000,
total_bytes=4096000,
download_speed=1024000.0,
)
await async_session.commit()
# Assert
assert updated.progress_percent == 75.5
assert updated.downloaded_bytes == 3072000
assert updated.total_bytes == 4096000
assert updated.download_speed == 1024000.0
async def test_status_transitions(self, async_session, test_series):
"""Test download status transitions."""
# Create pending item
item = await DownloadQueueService.create(
db=async_session,
series_id=test_series.id,
season=1,
episode_number=1,
)
await async_session.commit()
assert item.status == DownloadStatus.PENDING
# Transition to downloading
item = await DownloadQueueService.update_status(
async_session, item.id, DownloadStatus.DOWNLOADING
)
await async_session.commit()
assert item.status == DownloadStatus.DOWNLOADING
assert item.started_at is not None
# Transition to completed
item = await DownloadQueueService.update_status(
async_session, item.id, DownloadStatus.COMPLETED
)
await async_session.commit()
assert item.status == DownloadStatus.COMPLETED
assert item.completed_at is not None
async def test_failed_download_with_retry(self, async_session, test_series):
"""Test failed download with error message and retry count."""
# Create item
item = await DownloadQueueService.create(
db=async_session,
series_id=test_series.id,
season=1,
episode_number=1,
)
await async_session.commit()
# Mark as failed with error
item = await DownloadQueueService.update_status(
async_session,
item.id,
DownloadStatus.FAILED,
error_message="Connection timeout",
)
await async_session.commit()
# Assert
assert item.status == DownloadStatus.FAILED
assert item.error_message == "Connection timeout"
assert item.retry_count == 1
async def test_get_pending_items_ordered_by_priority(self, async_session, test_series):
"""Test retrieving pending items ordered by priority."""
# Create items with different priorities
await DownloadQueueService.create(
async_session, test_series.id, 1, 1, priority=DownloadPriority.LOW
)
await DownloadQueueService.create(
async_session, test_series.id, 1, 2, priority=DownloadPriority.HIGH
)
await DownloadQueueService.create(
async_session, test_series.id, 1, 3, priority=DownloadPriority.NORMAL
)
await async_session.commit()
# Get pending items
pending = await DownloadQueueService.get_pending(async_session)
# Assert order: HIGH -> NORMAL -> LOW
assert len(pending) == 3
assert pending[0].priority == DownloadPriority.HIGH
assert pending[1].priority == DownloadPriority.NORMAL
assert pending[2].priority == DownloadPriority.LOW
async def test_clear_completed_items(self, async_session, test_series):
"""Test clearing completed download items."""
# Create items
item1 = await DownloadQueueService.create(
async_session, test_series.id, 1, 1
)
item2 = await DownloadQueueService.create(
async_session, test_series.id, 1, 2
)
item3 = await DownloadQueueService.create(
async_session, test_series.id, 1, 3
)
# Complete first two
await DownloadQueueService.update_status(
async_session, item1.id, DownloadStatus.COMPLETED
)
await DownloadQueueService.update_status(
async_session, item2.id, DownloadStatus.COMPLETED
)
await async_session.commit()
# Clear completed
cleared = await DownloadQueueService.clear_completed(async_session)
await async_session.commit()
# Assert
assert cleared == 2
# Verify pending item remains
remaining = await DownloadQueueService.get_all(async_session)
assert len(remaining) == 1
assert remaining[0].id == item3.id
async def test_cascade_delete_with_series(self, async_session, test_series):
"""Test that queue items are deleted when series is deleted."""
# Create queue items
await DownloadQueueService.create(
async_session, test_series.id, 1, 1
)
await DownloadQueueService.create(
async_session, test_series.id, 1, 2
)
await async_session.commit()
# Delete series
await AnimeSeriesService.delete(async_session, test_series.id)
await async_session.commit()
# Verify queue items are gone
all_items = await DownloadQueueService.get_all(async_session)
assert len(all_items) == 0
```
---
### API Tests
**File:** `tests/api/test_queue_endpoints_database.py`
```python
"""API tests for queue endpoints with database persistence."""
import pytest
from httpx import AsyncClient
from unittest.mock import patch, AsyncMock
class TestQueueAPIWithDatabase:
"""Test queue API endpoints with database backend."""
@pytest.fixture
def auth_headers(self):
"""Get authentication headers."""
return {"Authorization": "Bearer test-token"}
async def test_get_queue_returns_database_items(
self, client: AsyncClient, auth_headers
):
"""Test GET /api/queue returns items from database."""
response = await client.get("/api/queue", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert "pending" in data
assert "active" in data
assert "completed" in data
async def test_add_to_queue_persists_to_database(
self, client: AsyncClient, auth_headers
):
"""Test POST /api/queue persists item to database."""
payload = {
"series_key": "test-anime",
"season": 1,
"episode": 1,
"priority": "normal",
}
response = await client.post(
"/api/queue",
json=payload,
headers=auth_headers,
)
assert response.status_code == 201
data = response.json()
assert "id" in data
async def test_remove_from_queue_deletes_from_database(
self, client: AsyncClient, auth_headers
):
"""Test DELETE /api/queue/{id} removes from database."""
# First add an item
add_response = await client.post(
"/api/queue",
json={"series_key": "test-anime", "season": 1, "episode": 1},
headers=auth_headers,
)
item_id = add_response.json()["id"]
# Then delete it
response = await client.delete(
f"/api/queue/{item_id}",
headers=auth_headers,
)
assert response.status_code == 200
# Verify it's gone
get_response = await client.get("/api/queue", headers=auth_headers)
queue_data = get_response.json()
item_ids = [item["id"] for item in queue_data.get("pending", [])]
assert item_id not in item_ids
async def test_queue_survives_server_restart(
self, client: AsyncClient, auth_headers
):
"""Test that queue items persist across simulated restart."""
# Add item
add_response = await client.post(
"/api/queue",
json={"series_key": "test-anime", "season": 1, "episode": 5},
headers=auth_headers,
)
item_id = add_response.json()["id"]
# Simulate restart by clearing in-memory cache
# (In real scenario, this would be a server restart)
# Verify item still exists
response = await client.get("/api/queue", headers=auth_headers)
queue_data = response.json()
item_ids = [item["id"] for item in queue_data.get("pending", [])]
assert item_id in item_ids
async def test_clear_completed_endpoint(
self, client: AsyncClient, auth_headers
):
"""Test POST /api/queue/clear-completed endpoint."""
response = await client.post(
"/api/queue/clear-completed",
headers=auth_headers,
)
assert response.status_code == 200
data = response.json()
assert "cleared_count" in data
```
---
### Performance Tests
**File:** `tests/performance/test_queue_database_performance.py`
```python
"""Performance tests for database-backed download queue."""
import pytest
import asyncio
import time
from datetime import datetime
class TestQueueDatabasePerformance:
"""Performance tests for queue database operations."""
@pytest.mark.performance
async def test_bulk_insert_performance(self, async_session, test_series):
"""Test performance of bulk queue item insertion."""
from src.server.database.service import DownloadQueueService
start_time = time.time()
# Insert 100 queue items
for i in range(100):
await DownloadQueueService.create(
async_session,
test_series.id,
season=1,
episode_number=i + 1,
)
await async_session.commit()
elapsed = time.time() - start_time
# Should complete in under 2 seconds
assert elapsed < 2.0, f"Bulk insert took {elapsed:.2f}s, expected < 2s"
@pytest.mark.performance
async def test_query_performance_with_many_items(self, async_session, test_series):
"""Test query performance with many queue items."""
from src.server.database.service import DownloadQueueService
# Setup: Create 500 items
for i in range(500):
await DownloadQueueService.create(
async_session,
test_series.id,
season=(i // 12) + 1,
episode_number=(i % 12) + 1,
)
await async_session.commit()
# Test query performance
start_time = time.time()
pending = await DownloadQueueService.get_pending(async_session)
elapsed = time.time() - start_time
# Query should complete in under 100ms
assert elapsed < 0.1, f"Query took {elapsed*1000:.1f}ms, expected < 100ms"
assert len(pending) == 500
@pytest.mark.performance
async def test_progress_update_performance(self, async_session, test_series):
"""Test performance of frequent progress updates."""
from src.server.database.service import DownloadQueueService
# Create item
item = await DownloadQueueService.create(
async_session, test_series.id, 1, 1
)
await async_session.commit()
start_time = time.time()
# Simulate 100 progress updates (like during download)
for i in range(100):
await DownloadQueueService.update_progress(
async_session,
item.id,
progress_percent=i,
downloaded_bytes=i * 10240,
total_bytes=1024000,
download_speed=102400.0,
)
await async_session.commit()
elapsed = time.time() - start_time
# 100 updates should complete in under 1 second
assert elapsed < 1.0, f"Progress updates took {elapsed:.2f}s, expected < 1s"
```
---
## Summary
These tasks will migrate the download queue from JSON file persistence to SQLite database, providing:
1. **Data Integrity**: ACID-compliant storage with proper relationships
2. **Query Capability**: Efficient filtering, sorting, and pagination
3. **Consistency**: Single source of truth for all application data
4. **Scalability**: Better performance for large queues
5. **Recovery**: Robust handling of crashes and restarts
The existing database infrastructure (`DownloadQueueItem` model and `DownloadQueueService`) is already in place, making this primarily an integration task rather than new development.