Task 3.3: Update ProgressService to use key as identifier

- Added optional 'key' and 'folder' fields to ProgressUpdate dataclass
- key: Primary series identifier (provider key, e.g., 'attack-on-titan')
- folder: Optional series folder name for display (e.g., 'Attack on Titan (2013)')
- Updated start_progress() and update_progress() methods to accept key/folder parameters
- Enhanced to_dict() serialization to include key/folder when present
- Updated all docstrings to clarify identifier usage
- Added 5 new comprehensive unit tests for key/folder functionality
- All 25 ProgressService tests passing
- Updated infrastructure.md with series identifier documentation
- Maintains backward compatibility - fields are optional
- Completed Phase 3, Task 3.3 of identifier standardization initiative
This commit is contained in:
Lukas 2025-11-27 18:36:35 +01:00
parent dda999fb98
commit fb2cdd4bb6
4 changed files with 179 additions and 9 deletions

View File

@ -955,23 +955,26 @@ Comprehensive test suite (`tests/unit/test_series_app.py`) with 22 tests coverin
### AnimeService Identifier Standardization (November 2025) ### AnimeService Identifier Standardization (November 2025)
Updated `AnimeService` to consistently use `key` as the primary series identifier, Updated `AnimeService` to consistently use `key` as the primary series identifier,
aligning with the broader identifier standardization initiative. aligning with the broader identifier standardization initiative.
#### Changes Made #### Changes Made
1. **Documentation Updates**: 1. **Documentation Updates**:
- Enhanced class docstring to clarify `key` vs `folder` usage - Enhanced class docstring to clarify `key` vs `folder` usage
- Updated all method docstrings to document identifier roles - Updated all method docstrings to document identifier roles
- `key`: Primary identifier for series lookups (provider-assigned, URL-safe) - `key`: Primary identifier for series lookups (provider-assigned, URL-safe)
- `folder`: Metadata only, used for display and filesystem operations - `folder`: Metadata only, used for display and filesystem operations
2. **Event Handler Clarification**: 2. **Event Handler Clarification**:
- `_on_download_status()`: Documents that events include both `key` and `serie_folder` - `_on_download_status()`: Documents that events include both `key` and `serie_folder`
- `_on_scan_status()`: Documents that events include both `key` and `folder` - `_on_scan_status()`: Documents that events include both `key` and `folder`
- Event handlers properly forward both identifiers to progress service - Event handlers properly forward both identifiers to progress service
3. **Method Documentation**: 3. **Method Documentation**:
- `list_missing()`: Returns series dicts with `key` as primary identifier - `list_missing()`: Returns series dicts with `key` as primary identifier
- `search()`: Returns results with `key` as identifier - `search()`: Returns results with `key` as identifier
- `rescan()`: Clarifies all series identified by `key` - `rescan()`: Clarifies all series identified by `key`
@ -984,11 +987,11 @@ aligning with the broader identifier standardization initiative.
#### Implementation Status #### Implementation Status
- ✅ All methods use `key` for series identification - ✅ All methods use `key` for series identification
- ✅ Event handlers properly receive and forward `key` field - ✅ Event handlers properly receive and forward `key` field
- ✅ Docstrings clearly document identifier usage - ✅ Docstrings clearly document identifier usage
- ✅ All anime service tests pass (18/18 passing) - ✅ All anime service tests pass (18/18 passing)
- ✅ Code follows project standards (PEP 8, type hints, docstrings) - ✅ Code follows project standards (PEP 8, type hints, docstrings)
**Task**: Phase 3, Task 3.2 - Update AnimeService to Use Key **Task**: Phase 3, Task 3.2 - Update AnimeService to Use Key
**Completion Date**: November 23, 2025 **Completion Date**: November 23, 2025
@ -1618,6 +1621,45 @@ A centralized service for tracking and broadcasting real-time progress updates a
- Global instance via `get_progress_service()` factory - Global instance via `get_progress_service()` factory
- Initialized during application startup with WebSocket callback - Initialized during application startup with WebSocket callback
**Series Identifier Support (November 2025)**:
- Added optional `key` and `folder` fields to `ProgressUpdate` dataclass
- `key`: Primary series identifier (provider key, e.g., 'attack-on-titan')
- `folder`: Optional series folder name (e.g., 'Attack on Titan (2013)')
- Both fields are included in progress events when series-related
- `to_dict()` serialization includes key/folder when present
- `start_progress()` and `update_progress()` accept key/folder parameters
- Maintains backward compatibility - fields are optional
**Usage Example**:
```python
# Start progress for a series download
await progress_service.start_progress(
progress_id="download-123",
progress_type=ProgressType.DOWNLOAD,
title="Downloading Attack on Titan",
key="attack-on-titan",
folder="Attack on Titan (2013)",
total=100
)
# Update progress
await progress_service.update_progress(
progress_id="download-123",
current=50,
message="Downloaded 50 MB"
)
```
**Test Coverage**:
- 25 comprehensive unit tests
- Tests verify key/folder serialization
- Tests verify key/folder preservation during updates
- Tests verify optional nature of fields
- All tests passing
#### Integration with Services #### Integration with Services
**DownloadService Integration**: **DownloadService Integration**:

View File

@ -1001,7 +1001,7 @@ conda run -n AniWorld python -m pytest tests/integration/test_identifier_consist
- [ ] Phase 3: Service Layer - [ ] Phase 3: Service Layer
- [x] Task 3.1: Update DownloadService ✅ **Completed November 2025** - [x] Task 3.1: Update DownloadService ✅ **Completed November 2025**
- [x] Task 3.2: Update AnimeService ✅ **Completed November 23, 2025** - [x] Task 3.2: Update AnimeService ✅ **Completed November 23, 2025**
- [ ] **Task 3.3: Update ProgressService** - [x] **Task 3.3: Update ProgressService** ✅ **Completed November 27, 2025**
- [ ] **Task 3.4: Update ScanService** - [ ] **Task 3.4: Update ScanService**
- [ ] Phase 4: API Layer - [ ] Phase 4: API Layer
- [ ] Task 4.1: Update Anime API Endpoints - [ ] Task 4.1: Update Anime API Endpoints

View File

@ -51,6 +51,10 @@ class ProgressUpdate:
percent: Completion percentage (0-100) percent: Completion percentage (0-100)
current: Current progress value current: Current progress value
total: Total progress value total: Total progress value
key: Optional series identifier (provider key, e.g., 'attack-on-titan')
Used as the primary identifier for series-related operations
folder: Optional series folder name (e.g., 'Attack on Titan (2013)')
Used for display and filesystem operations only
metadata: Additional metadata metadata: Additional metadata
started_at: When operation started started_at: When operation started
updated_at: When last updated updated_at: When last updated
@ -64,13 +68,20 @@ class ProgressUpdate:
percent: float = 0.0 percent: float = 0.0
current: int = 0 current: int = 0
total: int = 0 total: int = 0
key: Optional[str] = None
folder: Optional[str] = None
metadata: Dict[str, Any] = field(default_factory=dict) metadata: Dict[str, Any] = field(default_factory=dict)
started_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) started_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
def to_dict(self) -> Dict[str, Any]: def to_dict(self) -> Dict[str, Any]:
"""Convert progress update to dictionary.""" """Convert progress update to dictionary.
return {
Returns:
Dictionary representation with all fields including optional
key (series identifier) and folder (display metadata).
"""
result = {
"id": self.id, "id": self.id,
"type": self.type.value, "type": self.type.value,
"status": self.status.value, "status": self.status.value,
@ -84,6 +95,14 @@ class ProgressUpdate:
"started_at": self.started_at.isoformat(), "started_at": self.started_at.isoformat(),
"updated_at": self.updated_at.isoformat(), "updated_at": self.updated_at.isoformat(),
} }
# Include optional series identifier fields
if self.key is not None:
result["key"] = self.key
if self.folder is not None:
result["folder"] = self.folder
return result
@dataclass @dataclass
@ -220,6 +239,8 @@ class ProgressService:
title: str, title: str,
total: int = 0, total: int = 0,
message: str = "", message: str = "",
key: Optional[str] = None,
folder: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None, metadata: Optional[Dict[str, Any]] = None,
) -> ProgressUpdate: ) -> ProgressUpdate:
"""Start a new progress operation. """Start a new progress operation.
@ -230,6 +251,10 @@ class ProgressService:
title: Human-readable title title: Human-readable title
total: Total items/bytes to process total: Total items/bytes to process
message: Initial message message: Initial message
key: Optional series identifier (provider key)
Used as primary identifier for series-related operations
folder: Optional series folder name
Used for display and filesystem operations only
metadata: Additional metadata metadata: Additional metadata
Returns: Returns:
@ -251,6 +276,8 @@ class ProgressService:
title=title, title=title,
message=message, message=message,
total=total, total=total,
key=key,
folder=folder,
metadata=metadata or {}, metadata=metadata or {},
) )
@ -261,6 +288,8 @@ class ProgressService:
progress_id=progress_id, progress_id=progress_id,
type=progress_type.value, type=progress_type.value,
title=title, title=title,
key=key,
folder=folder,
) )
# Emit event to subscribers # Emit event to subscribers
@ -281,6 +310,8 @@ class ProgressService:
current: Optional[int] = None, current: Optional[int] = None,
total: Optional[int] = None, total: Optional[int] = None,
message: Optional[str] = None, message: Optional[str] = None,
key: Optional[str] = None,
folder: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None, metadata: Optional[Dict[str, Any]] = None,
force_broadcast: bool = False, force_broadcast: bool = False,
) -> ProgressUpdate: ) -> ProgressUpdate:
@ -291,6 +322,8 @@ class ProgressService:
current: Current progress value current: Current progress value
total: Updated total value total: Updated total value
message: Updated message message: Updated message
key: Optional series identifier (provider key)
folder: Optional series folder name
metadata: Additional metadata to merge metadata: Additional metadata to merge
force_broadcast: Force broadcasting even for small changes force_broadcast: Force broadcasting even for small changes
@ -316,6 +349,10 @@ class ProgressService:
update.total = total update.total = total
if message is not None: if message is not None:
update.message = message update.message = message
if key is not None:
update.key = key
if folder is not None:
update.folder = folder
if metadata: if metadata:
update.metadata.update(metadata) update.metadata.update(metadata)

View File

@ -508,3 +508,94 @@ class TestProgressService:
assert progress.metadata["initial"] == "value" assert progress.metadata["initial"] == "value"
assert progress.metadata["additional"] == "data" assert progress.metadata["additional"] == "data"
assert progress.metadata["speed"] == 1.5 assert progress.metadata["speed"] == 1.5
@pytest.mark.asyncio
async def test_progress_with_key_and_folder(self, service):
"""Test progress tracking with series key and folder."""
# Start progress with key and folder
update = await service.start_progress(
progress_id="download-series-1",
progress_type=ProgressType.DOWNLOAD,
title="Downloading Attack on Titan",
key="attack-on-titan",
folder="Attack on Titan (2013)",
total=100,
)
assert update.key == "attack-on-titan"
assert update.folder == "Attack on Titan (2013)"
# Verify to_dict includes key and folder
dict_repr = update.to_dict()
assert dict_repr["key"] == "attack-on-titan"
assert dict_repr["folder"] == "Attack on Titan (2013)"
# Update progress and verify key/folder are preserved
updated = await service.update_progress(
progress_id="download-series-1",
current=50,
)
assert updated.key == "attack-on-titan"
assert updated.folder == "Attack on Titan (2013)"
@pytest.mark.asyncio
async def test_progress_update_key_and_folder(self, service):
"""Test updating key and folder in existing progress."""
# Start without key/folder
await service.start_progress(
progress_id="test-1",
progress_type=ProgressType.SCAN,
title="Test Scan",
)
# Update with key and folder
updated = await service.update_progress(
progress_id="test-1",
key="one-piece",
folder="One Piece (1999)",
current=10,
)
assert updated.key == "one-piece"
assert updated.folder == "One Piece (1999)"
# Verify to_dict includes the fields
dict_repr = updated.to_dict()
assert dict_repr["key"] == "one-piece"
assert dict_repr["folder"] == "One Piece (1999)"
def test_progress_update_to_dict_without_key_folder(self):
"""Test to_dict doesn't include key/folder if not set."""
update = ProgressUpdate(
id="test-1",
type=ProgressType.SYSTEM,
status=ProgressStatus.STARTED,
title="System Task",
)
result = update.to_dict()
# key and folder should not be in dict if not set
assert "key" not in result
assert "folder" not in result
def test_progress_update_creation_with_key_folder(self):
"""Test creating progress update with key and folder."""
update = ProgressUpdate(
id="test-1",
type=ProgressType.DOWNLOAD,
status=ProgressStatus.STARTED,
title="Test Download",
key="naruto",
folder="Naruto (2002)",
total=100,
)
assert update.key == "naruto"
assert update.folder == "Naruto (2002)"
# Verify to_dict includes them
result = update.to_dict()
assert result["key"] == "naruto"
assert result["folder"] == "Naruto (2002)"