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:
parent
dda999fb98
commit
fb2cdd4bb6
@ -961,17 +961,20 @@ 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`
|
||||||
@ -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**:
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
@ -85,6 +96,14 @@ class ProgressUpdate:
|
|||||||
"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
|
||||||
class ProgressEvent:
|
class ProgressEvent:
|
||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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)"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user