This commit is contained in:
Lukas 2025-10-22 13:38:46 +02:00
parent 1f39f07c5d
commit 04799633b4
9 changed files with 411 additions and 571 deletions

View File

@ -1,204 +0,0 @@
# Quality Improvements - Implementation Summary
## Date: October 22, 2025
### Critical Issues Resolved
#### 1. ✅ Fixed Missing Import Error (CRITICAL)
**File**: `src/core/providers/enhanced_provider.py`
- **Issue**: Import error from non-existent `error_handler` module causing runtime failure
- **Solution**: Created stub module at `src/core/error_handler.py` with:
- `RetryableError`, `NonRetryableError`, `NetworkError`, `DownloadError` exception classes
- `with_error_recovery` decorator for automatic retry logic
- `RecoveryStrategies` class with network and download failure handling
- `FileCorruptionDetector` class for video file validation
- **Impact**: Application now starts without import errors ✅
#### 2. ✅ Fixed CORS Configuration (SECURITY)
**File**: `src/server/fastapi_app.py`
- **Issue**: `allow_origins=["*"]` exposes API to any domain (production security risk)
- **Solution**:
- Updated to use environment-based `CORS_ORIGINS` setting
- Defaults to localhost for development
- Production can configure via `.env` file
- Added security warning in code comments
- **Impact**: Improved API security and configurability ✅
#### 3. ✅ Fixed Type Hint Syntax Errors (PEP8)
**Files**:
- `src/core/providers/streaming/Provider.py`
- `src/core/providers/streaming/voe.py`
- `src/core/providers/streaming/doodstream.py`
**Issues Fixed**:
- Invalid return type `(str, [str])` → corrected to `tuple[str, dict[str, Any]]`
- Method names not following PEP8:
- `GetLink()``get_link()`
- `embededLink``embedded_link`
- `DEFAULT_REQUEST_TIMEOUT``timeout`
**Changes**:
- Base `Provider` abstract class with proper type hints
- Updated all implementations (VOE, Doodstream) with correct signatures
- Updated all callers to use new method names
- Added comprehensive docstrings for all methods
**Impact**: Fixed ~12 type checking violations ✅
#### 4. ✅ Enhanced Base Provider Documentation (PEP257)
**File**: `src/core/providers/base_provider.py`
**Improvements**:
- Added comprehensive type hints to all abstract methods
- Enhanced docstrings for all parameters with clear descriptions
- Added return type annotations with detailed documentation
- Parameters now include type annotations and context
- Example: `get_season_episode_count()` now has full parameter and return documentation
**Impact**: Improved code discoverability and maintainability ✅
### Quality Metrics Improved
| Metric | Before | After | Status |
| ----------------------- | ---------- | ------------- | ------ |
| Import Errors | 1 Critical | 0 | ✅ |
| CORS Security Risk | High | Resolved | ✅ |
| Type Hint Syntax Errors | 12+ | 0 | ✅ |
| Abstract Method Docs | Minimal | Comprehensive | ✅ |
| Tests Passing | Yes | Yes | ✅ |
### Files Modified
1. **Created**: `src/core/error_handler.py` (100 lines)
- New module for error handling infrastructure
- Provides recovery strategies and custom exceptions
2. **Modified**: `src/core/providers/streaming/Provider.py`
- Added proper type hints
- Enhanced documentation
3. **Modified**: `src/core/providers/streaming/voe.py`
- Method naming: GetLink → get_link
- Parameter naming: embededLink → embedded_link, etc.
- Return type: now returns tuple instead of just string
- Added type hints throughout
4. **Modified**: `src/core/providers/streaming/doodstream.py`
- Method naming: GetLink → get_link
- Parameter naming consistency
- Return type: now returns tuple with headers
- Added type hints and docstrings
5. **Modified**: `src/server/fastapi_app.py`
- CORS configuration now environment-based
- Added security warning comment
- Uses settings.cors_origins for configuration
6. **Modified**: `src/core/providers/base_provider.py`
- Added comprehensive type hints to all abstract methods
- Enhanced docstrings with parameter descriptions
- Added Optional and Callable type imports
7. **Modified**: `src/core/providers/aniworld_provider.py`
- Updated GetLink call → get_link
8. **Modified**: `src/core/providers/enhanced_provider.py`
- Updated GetLink call → get_link
- Fixed import to use new error_handler module
### Test Results
All tests passing after changes:
- ✅ Unit tests: PASS
- ✅ Integration tests: PASS
- ✅ API tests: PASS
- ✅ No regressions detected
### Remaining Quality Issues (For Future Work)
See `QualityTODO.md` for comprehensive list. Priority items:
**High Priority**:
- [ ] Sort imports with isort in CLI and provider modules
- [ ] Add type hints to CLI methods (search, get_user_selection, etc.)
- [ ] Remove duplicate SeriesApp class from CLI
**Medium Priority**:
- [ ] Rate limiter memory leak cleanup
- [ ] Database query optimization (N+1 problems)
- [ ] String operation efficiency improvements
**Low Priority**:
- [ ] Performance profiling and optimization
- [ ] Additional security hardening
- [ ] Extended documentation
### Code Quality Standards Applied
**PEP 8 Compliance**:
- Type hints on all modified method signatures
- Proper spacing between methods and classes
- Line length compliance
**PEP 257 Compliance**:
- Comprehensive docstrings on abstract methods
- Parameter documentation
- Return type documentation
**Security**:
- CORS properly configured
- Error handling infrastructure in place
- Input validation patterns established
**Performance**:
- No blocking changes
- Efficient error recovery mechanisms
- Proper use of async patterns
### Validation Checklist
- [x] No syntax errors
- [x] All imports resolve correctly
- [x] Type hints are valid
- [x] All tests pass
- [x] No regressions
- [x] Security improvements applied
- [x] Documentation enhanced
### Next Steps Recommended
1. Run `isort` on remaining files to standardize imports
2. Add type hints to CLI module methods
3. Implement periodic cleanup for rate limiter
4. Add database query optimization
5. Profile performance under load
---
**Completed by**: GitHub Copilot Assistant
**Verification**: All tests passing, no blockers, ready for production deployment

View File

@ -78,28 +78,6 @@ conda run -n AniWorld python -m pytest tests/ -v -s
### 1⃣ Code Follows PEP8 and Project Coding Standards
#### Naming Convention Issues
- [ ] `src/core/providers/streaming/Provider.py` - PENDING
- `GetLink()` should be `get_link()`
- Also has invalid type hint syntax that needs fixing
- [ ] `src/core/providers/enhanced_provider.py` - PENDING
- Similar naming convention issues as aniworld_provider
- Needs parallel refactoring
#### Import Sorting and Organization
- [ ] `src/cli/Main.py` - Imports not in isort order
- Should group: stdlib, third-party, local imports
- Line 1-11 needs reorganization
- [ ] `src/core/providers/aniworld_provider.py` - Imports not sorted
- [ ] `src/core/providers/enhanced_provider.py` - Imports not sorted
- [ ] `src/server/api/download.py` - Verify import order
- [ ] Run `isort --check-only` on entire codebase to identify violations
#### Blank Line Spacing Issues (PEP8 §4)
- [ ] `src/cli/Main.py` - Missing blank lines between methods

View File

@ -1,108 +0,0 @@
# Test Verification Report
**Date**: October 22, 2025
**Status**: ✅ ALL TESTS PASSING
## Test Results Summary
### ✅ All Tests Suite
- **Command**: `pytest tests/ -v --tb=short`
- **Status**: ✅ PASSED
- **Exit Code**: 0
### ✅ Unit Tests
- **Command**: `pytest tests/unit/ -v`
- **Status**: ✅ PASSED
- **Exit Code**: 0
- **Coverage**: All unit tests in `tests/unit/` directory
### ✅ Integration Tests
- **Command**: `pytest tests/integration/ -v`
- **Status**: ✅ PASSED
- **Exit Code**: 0
- **Coverage**: All integration tests in `tests/integration/` directory
## Quality Improvements Applied
The following changes were made and all tests continue to pass:
1. ✅ Created `src/core/error_handler.py` with:
- Custom exception classes
- Error recovery decorators
- Network failure handling
- File corruption detection
2. ✅ Fixed CORS configuration in `src/server/fastapi_app.py`
- Environment-based configuration
- Security improvements
3. ✅ Fixed type hints in provider modules:
- `src/core/providers/streaming/Provider.py`
- `src/core/providers/streaming/voe.py`
- `src/core/providers/streaming/doodstream.py`
4. ✅ Enhanced documentation in:
- `src/core/providers/base_provider.py`
- All provider implementations
5. ✅ Updated method calls and imports across:
- `src/core/providers/aniworld_provider.py`
- `src/core/providers/enhanced_provider.py`
- `src/server/fastapi_app.py`
## Verification Checklist
- [x] All unit tests passing
- [x] All integration tests passing
- [x] All API tests passing
- [x] No import errors
- [x] No type checking errors
- [x] No regression issues
- [x] Code quality improvements applied
- [x] Security enhancements in place
## Test Command Reference
```bash
# Run all tests
conda run -n AniWorld python -m pytest tests/ -v --tb=short
# Run unit tests only
conda run -n AniWorld python -m pytest tests/unit/ -v
# Run integration tests only
conda run -n AniWorld python -m pytest tests/integration/ -v
# Run with coverage
conda run -n AniWorld python -m pytest tests/ -v --cov=src
# Run with verbose output
conda run -n AniWorld python -m pytest tests/ -vv
```
## Next Steps
The codebase is now in a stable state with:
- ✅ All tests passing
- ✅ Critical security issues fixed
- ✅ Type hints improved
- ✅ Documentation enhanced
- ✅ No regressions
Ready for:
- Deployment to production
- Further feature development
- Additional quality improvements from the QualityTODO.md list
---
**Last Verified**: October 22, 2025 - All systems operational ✅

View File

@ -1,7 +1,7 @@
{
"pending": [
{
"id": "42233fa7-50b3-4941-882c-be23e776e88c",
"id": "a99e0a91-c71c-49c6-a26b-f0682643b61f",
"serie_id": "workflow-series",
"serie_name": "Workflow Test Series",
"episode": {
@ -11,7 +11,7 @@
},
"status": "pending",
"priority": "high",
"added_at": "2025-10-22T10:57:26.642934Z",
"added_at": "2025-10-22T11:08:17.906509Z",
"started_at": null,
"completed_at": null,
"progress": null,
@ -20,7 +20,7 @@
"source_url": null
},
{
"id": "eeafcd93-9df7-4429-872c-1220a642b775",
"id": "564070a3-6548-4238-96d3-05f3fc9a0a6b",
"serie_id": "series-2",
"serie_name": "Series 2",
"episode": {
@ -30,7 +30,7 @@
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-22T10:57:26.331686Z",
"added_at": "2025-10-22T11:08:17.631588Z",
"started_at": null,
"completed_at": null,
"progress": null,
@ -39,7 +39,7 @@
"source_url": null
},
{
"id": "21b6d879-7478-4711-8a91-647383cc2d64",
"id": "3587a189-0b04-4954-b480-20530cf5fdb9",
"serie_id": "series-1",
"serie_name": "Series 1",
"episode": {
@ -49,7 +49,7 @@
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-22T10:57:26.326103Z",
"added_at": "2025-10-22T11:08:17.629702Z",
"started_at": null,
"completed_at": null,
"progress": null,
@ -58,7 +58,7 @@
"source_url": null
},
{
"id": "68ade854-067f-4486-bfb3-f0d0ce2cdb88",
"id": "18c6c1dc-279e-44d7-b032-6f14aa061cc3",
"serie_id": "series-0",
"serie_name": "Series 0",
"episode": {
@ -68,7 +68,7 @@
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-22T10:57:26.321881Z",
"added_at": "2025-10-22T11:08:17.626684Z",
"started_at": null,
"completed_at": null,
"progress": null,
@ -77,7 +77,7 @@
"source_url": null
},
{
"id": "bf18f799-ce21-4f06-bf8c-cc4a26b815f5",
"id": "0e47ef3d-e233-4631-bea3-a1d15ac9b2ad",
"serie_id": "series-high",
"serie_name": "Series High",
"episode": {
@ -87,7 +87,7 @@
},
"status": "pending",
"priority": "high",
"added_at": "2025-10-22T10:57:26.077002Z",
"added_at": "2025-10-22T11:08:17.404606Z",
"started_at": null,
"completed_at": null,
"progress": null,
@ -96,7 +96,7 @@
"source_url": null
},
{
"id": "daebc904-6c32-4ca5-947c-18d25d43fdfe",
"id": "e09d09f4-46d7-4408-960b-1508389f0e5a",
"serie_id": "test-series-2",
"serie_name": "Another Series",
"episode": {
@ -106,7 +106,7 @@
},
"status": "pending",
"priority": "high",
"added_at": "2025-10-22T10:57:26.051169Z",
"added_at": "2025-10-22T11:08:17.379463Z",
"started_at": null,
"completed_at": null,
"progress": null,
@ -115,7 +115,7 @@
"source_url": null
},
{
"id": "aee9fdf2-4436-43ad-8c08-1bcdc2013bb4",
"id": "9e3eac5f-3d39-45b3-8584-96bcf9901af9",
"serie_id": "test-series-1",
"serie_name": "Test Anime Series",
"episode": {
@ -125,7 +125,7 @@
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-22T10:57:26.023723Z",
"added_at": "2025-10-22T11:08:17.354951Z",
"started_at": null,
"completed_at": null,
"progress": null,
@ -134,7 +134,7 @@
"source_url": null
},
{
"id": "c45a72c3-11af-48dd-ba23-2f2d93bd0d3c",
"id": "6cb7f8f0-ba85-4778-a7af-2d0b43e26dcb",
"serie_id": "test-series-1",
"serie_name": "Test Anime Series",
"episode": {
@ -144,7 +144,7 @@
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-22T10:57:26.023820Z",
"added_at": "2025-10-22T11:08:17.355041Z",
"started_at": null,
"completed_at": null,
"progress": null,
@ -153,7 +153,7 @@
"source_url": null
},
{
"id": "83e1e2bc-a1b5-49a2-a183-421884c183ce",
"id": "c43d205f-70f8-48d1-bde7-5f3e57cd0775",
"serie_id": "series-normal",
"serie_name": "Series Normal",
"episode": {
@ -163,7 +163,7 @@
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-22T10:57:26.082663Z",
"added_at": "2025-10-22T11:08:17.406642Z",
"started_at": null,
"completed_at": null,
"progress": null,
@ -172,7 +172,7 @@
"source_url": null
},
{
"id": "9a78d1b5-9e23-4e79-b4e4-40bf0a4ce8a1",
"id": "841046e8-9bd9-45e6-b53c-127a97918568",
"serie_id": "series-low",
"serie_name": "Series Low",
"episode": {
@ -182,7 +182,7 @@
},
"status": "pending",
"priority": "low",
"added_at": "2025-10-22T10:57:26.084695Z",
"added_at": "2025-10-22T11:08:17.410918Z",
"started_at": null,
"completed_at": null,
"progress": null,
@ -191,7 +191,7 @@
"source_url": null
},
{
"id": "d2f8fa60-9806-45d0-8ca8-8bf46499e9e7",
"id": "515d07ee-7ab6-4b37-acd9-cc4cc8066141",
"serie_id": "test-series",
"serie_name": "Test Series",
"episode": {
@ -201,7 +201,7 @@
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-22T10:57:26.269365Z",
"added_at": "2025-10-22T11:08:17.579360Z",
"started_at": null,
"completed_at": null,
"progress": null,
@ -210,7 +210,7 @@
"source_url": null
},
{
"id": "1605e153-67e5-4a47-b5a2-e721cbbfe609",
"id": "52ca17ce-3c35-454d-b0e7-4fc72da50282",
"serie_id": "test-series",
"serie_name": "Test Series",
"episode": {
@ -220,7 +220,7 @@
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-22T10:57:26.360496Z",
"added_at": "2025-10-22T11:08:17.656563Z",
"started_at": null,
"completed_at": null,
"progress": null,
@ -229,7 +229,7 @@
"source_url": null
},
{
"id": "c10c1bf1-ccab-45bb-8526-38da70efb337",
"id": "f8688ae7-2a1c-42a0-88af-29d5e2c17542",
"serie_id": "invalid-series",
"serie_name": "Invalid Series",
"episode": {
@ -239,7 +239,7 @@
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-22T10:57:26.417614Z",
"added_at": "2025-10-22T11:08:17.705743Z",
"started_at": null,
"completed_at": null,
"progress": null,
@ -248,7 +248,7 @@
"source_url": null
},
{
"id": "449e2572-c506-4f22-878f-24637b64ac89",
"id": "e93f5198-cb70-4e6a-b19e-c13b52286027",
"serie_id": "test-series",
"serie_name": "Test Series",
"episode": {
@ -258,7 +258,7 @@
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-22T10:57:26.442816Z",
"added_at": "2025-10-22T11:08:17.731256Z",
"started_at": null,
"completed_at": null,
"progress": null,
@ -267,45 +267,7 @@
"source_url": null
},
{
"id": "cf9ad8ab-deeb-400c-8fc8-791fe7ce2dc5",
"serie_id": "series-1",
"serie_name": "Series 1",
"episode": {
"season": 1,
"episode": 1,
"title": null
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-22T10:57:26.483725Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "62908e0e-2367-4472-b00d-0fc43e725b79",
"serie_id": "series-3",
"serie_name": "Series 3",
"episode": {
"season": 1,
"episode": 1,
"title": null
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-22T10:57:26.484579Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "06eca34e-f597-4464-b8ce-e0b631ec3b92",
"id": "bc8d7f8f-b283-4364-9cdc-c4745d2c182a",
"serie_id": "series-4",
"serie_name": "Series 4",
"episode": {
@ -315,7 +277,7 @@
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-22T10:57:26.485459Z",
"added_at": "2025-10-22T11:08:17.765884Z",
"started_at": null,
"completed_at": null,
"progress": null,
@ -324,7 +286,26 @@
"source_url": null
},
{
"id": "86b4443a-4d5b-4aa9-b435-4227485a0ee3",
"id": "0d75d9d4-cb44-4625-927d-d173d0810fe7",
"serie_id": "series-3",
"serie_name": "Series 3",
"episode": {
"season": 1,
"episode": 1,
"title": null
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-22T11:08:17.767550Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "a5e454ad-b044-4b56-af25-3cfc1182b5ee",
"serie_id": "series-2",
"serie_name": "Series 2",
"episode": {
@ -334,7 +315,7 @@
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-22T10:57:26.486109Z",
"added_at": "2025-10-22T11:08:17.768218Z",
"started_at": null,
"completed_at": null,
"progress": null,
@ -343,7 +324,26 @@
"source_url": null
},
{
"id": "68b49911-bb50-480c-ac60-fb679c381ffb",
"id": "06a5c4be-1b03-4098-8577-19b8b9f39d74",
"serie_id": "series-1",
"serie_name": "Series 1",
"episode": {
"season": 1,
"episode": 1,
"title": null
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-22T11:08:17.768875Z",
"started_at": null,
"completed_at": null,
"progress": null,
"error": null,
"retry_count": 0,
"source_url": null
},
{
"id": "fe20606e-7a8e-4378-a45e-e62148473729",
"serie_id": "series-0",
"serie_name": "Series 0",
"episode": {
@ -353,7 +353,7 @@
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-22T10:57:26.486790Z",
"added_at": "2025-10-22T11:08:17.769549Z",
"started_at": null,
"completed_at": null,
"progress": null,
@ -362,7 +362,7 @@
"source_url": null
},
{
"id": "800ca4df-12c0-4ebf-a5d6-139732d22fd6",
"id": "cd33d997-7e96-4105-9267-06811cc20439",
"serie_id": "persistent-series",
"serie_name": "Persistent Series",
"episode": {
@ -372,7 +372,7 @@
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-22T10:57:26.556798Z",
"added_at": "2025-10-22T11:08:17.829367Z",
"started_at": null,
"completed_at": null,
"progress": null,
@ -381,7 +381,7 @@
"source_url": null
},
{
"id": "3c693ed7-6a9e-4afd-a71e-99ec549a4d00",
"id": "89c65c60-a936-44a0-a0be-6c97fd9ce5a7",
"serie_id": "ws-series",
"serie_name": "WebSocket Series",
"episode": {
@ -391,7 +391,7 @@
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-22T10:57:26.615000Z",
"added_at": "2025-10-22T11:08:17.880458Z",
"started_at": null,
"completed_at": null,
"progress": null,
@ -400,7 +400,7 @@
"source_url": null
},
{
"id": "3b1ae938-e470-4449-8e04-9c576a55f636",
"id": "031ecfa1-e940-407f-b52f-e532147fbd99",
"serie_id": "pause-test",
"serie_name": "Pause Test Series",
"episode": {
@ -410,7 +410,7 @@
},
"status": "pending",
"priority": "normal",
"added_at": "2025-10-22T10:57:26.778755Z",
"added_at": "2025-10-22T11:08:18.039451Z",
"started_at": null,
"completed_at": null,
"progress": null,
@ -421,5 +421,5 @@
],
"active": [],
"failed": [],
"timestamp": "2025-10-22T10:57:26.779584+00:00"
"timestamp": "2025-10-22T11:08:18.039697+00:00"
}

View File

@ -1,13 +1,12 @@
import logging
import os
import sys
import time
from typing import Any, Callable, Mapping, Optional, Sequence
from rich.progress import Progress
from ..core.entities import SerieList
from ..core.entities.series import Serie
from ..core.providers import aniworld_provider
from ..core.providers.provider_factory import Loaders
from ..core.SerieScanner import SerieScanner
@ -30,35 +29,37 @@ for h in logging.getLogger().handlers:
class NoKeyFoundException(Exception):
"""Exception raised when an anime key cannot be found."""
pass
class MatchNotFoundError(Exception):
"""Exception raised when an anime key cannot be found."""
pass
class SeriesApp:
_initialization_count = 0 # Track how many times initialization has been called
_initialization_count = 0 # Track initialization calls
def __init__(self, directory_to_search: str):
def __init__(self, directory_to_search: str) -> None:
SeriesApp._initialization_count += 1
# Only show initialization message for the first instance
if SeriesApp._initialization_count <= 1:
print("Please wait while initializing...")
self.progress = None
self.progress: Optional[Progress] = None
self.directory_to_search = directory_to_search
self.Loaders = Loaders()
self.Loaders: Loaders = Loaders()
loader = self.Loaders.GetLoader(key="aniworld.to")
self.SerieScanner = SerieScanner(directory_to_search, loader)
self.List = SerieList(self.directory_to_search)
self.__init_list__()
def __init_list__(self):
def __init_list__(self) -> None:
"""Initialize the series list by fetching missing episodes."""
self.series_list = self.List.GetMissingEpisode()
self.series_list: Sequence[Serie] = self.List.GetMissingEpisode()
def display_series(self):
def display_series(self) -> None:
"""Print all series with assigned numbers."""
print("\nCurrent result:")
for i, serie in enumerate(self.series_list, 1):
@ -68,12 +69,12 @@ class SeriesApp:
else:
print(f"{i}. {serie.name}")
def search(self, words: str) -> list:
def search(self, words: str) -> list[dict[str, Any]]:
"""Search for anime series by name."""
loader = self.Loaders.GetLoader(key="aniworld.to")
return loader.search(words)
def get_user_selection(self):
def get_user_selection(self) -> Optional[Sequence[Serie]]:
"""Handle user input for selecting series."""
self.display_series()
while True:
@ -86,9 +87,9 @@ class SeriesApp:
if selection == "exit":
return None
selected_series = []
selected_series: list[Serie] = []
if selection == "all":
selected_series = self.series_list
selected_series = list(self.series_list)
else:
try:
indexes = [
@ -118,7 +119,14 @@ class SeriesApp:
print(msg)
return None
def retry(self, func, max_retries=3, delay=2, *args, **kwargs):
def retry(
self,
func: Callable[..., Any],
max_retries: int = 3,
delay: float = 2,
*args: Any,
**kwargs: Any,
) -> bool:
"""Retry a function with exponential backoff.
Args:
@ -140,7 +148,7 @@ class SeriesApp:
time.sleep(delay)
return False
def download_series(self, series):
def download_series(self, series: Sequence[Serie]) -> None:
"""Simulate the downloading process with a progress bar."""
total_downloaded = 0
total_episodes = sum(
@ -182,7 +190,7 @@ class SeriesApp:
episode,
serie.key,
"German Dub",
self.print_Download_Progress,
self.print_download_progress,
)
downloaded += 1
@ -195,20 +203,24 @@ class SeriesApp:
self.progress.stop()
self.progress = None
def print_download_progress(self, d):
def print_download_progress(self, d: Mapping[str, Any]) -> None:
"""Update download progress in the UI.
Args:
d: Dictionary containing download status information
"""
# Use self.progress and self.download_progress_task to display progress
if (self.progress is None or
not hasattr(self, "download_progress_task")):
if (
self.progress is None
or not hasattr(self, "download_progress_task")
):
return
if d["status"] == "downloading":
total = (d.get("total_bytes") or
d.get("total_bytes_estimate"))
total = (
d.get("total_bytes")
or d.get("total_bytes_estimate")
)
downloaded = d.get("downloaded_bytes", 0)
if total:
percent = downloaded / total * 100
@ -232,7 +244,7 @@ class SeriesApp:
description=desc
)
def search_mode(self):
def search_mode(self) -> None:
"""Search for a series and allow user to select an option."""
search_string = input("Enter search string: ").strip()
results = self.search(search_string)
@ -272,10 +284,10 @@ class SeriesApp:
except ValueError:
print("Invalid input. Try again.")
def updateFromReinit(self, folder, counter):
def updateFromReinit(self, folder: str, counter: int) -> None:
self.progress.update(self.task1, advance=1)
def run(self):
def run(self) -> None:
"""Main function to run the app."""
while True:
prompt = (

View File

@ -10,7 +10,7 @@ import os
import re
import traceback
import uuid
from typing import Callable, Optional
from typing import Callable, Iterable, Iterator, Optional
from src.core.entities.series import Serie
from src.core.exceptions.Exceptions import MatchNotFoundError, NoKeyFoundException
@ -40,7 +40,7 @@ class SerieScanner:
basePath: str,
loader: Loader,
callback_manager: Optional[CallbackManager] = None
):
) -> None:
"""
Initialize the SerieScanner.
@ -49,10 +49,12 @@ class SerieScanner:
loader: Loader instance for fetching series information
callback_manager: Optional callback manager for progress updates
"""
self.directory = basePath
self.directory: str = basePath
self.folderDict: dict[str, Serie] = {}
self.loader = loader
self._callback_manager = callback_manager or CallbackManager()
self.loader: Loader = loader
self._callback_manager: CallbackManager = (
callback_manager or CallbackManager()
)
self._current_operation_id: Optional[str] = None
logger.info("Initialized SerieScanner with base path: %s", basePath)
@ -62,22 +64,22 @@ class SerieScanner:
"""Get the callback manager instance."""
return self._callback_manager
def reinit(self):
def reinit(self) -> None:
"""Reinitialize the folder dictionary."""
self.folderDict: dict[str, Serie] = {}
def is_null_or_whitespace(self, s):
def is_null_or_whitespace(self, value: Optional[str]) -> bool:
"""Check if a string is None or whitespace.
Args:
s: String value to check
value: String value to check
Returns:
True if string is None or contains only whitespace
"""
return s is None or s.strip() == ""
return value is None or value.strip() == ""
def get_total_to_scan(self):
def get_total_to_scan(self) -> int:
"""Get the total number of folders to scan.
Returns:
@ -86,7 +88,10 @@ class SerieScanner:
result = self.__find_mp4_files()
return sum(1 for _ in result)
def scan(self, callback: Optional[Callable[[str, int], None]] = None):
def scan(
self,
callback: Optional[Callable[[str, int], None]] = None
) -> None:
"""
Scan directories for anime series and missing episodes.
@ -127,10 +132,10 @@ class SerieScanner:
counter += 1
# Calculate progress
percentage = (
(counter / total_to_scan * 100)
if total_to_scan > 0 else 0
)
if total_to_scan > 0:
percentage = (counter / total_to_scan) * 100
else:
percentage = 0.0
# Notify progress
self._callback_manager.notify_progress(
@ -262,13 +267,13 @@ class SerieScanner:
raise
def __find_mp4_files(self):
def __find_mp4_files(self) -> Iterator[tuple[str, list[str]]]:
"""Find all .mp4 files in the directory structure."""
logger.info("Scanning for .mp4 files")
for anime_name in os.listdir(self.directory):
anime_path = os.path.join(self.directory, anime_name)
if os.path.isdir(anime_path):
mp4_files = []
mp4_files: list[str] = []
has_files = False
for root, _, files in os.walk(anime_path):
for file in files:
@ -277,7 +282,7 @@ class SerieScanner:
has_files = True
yield anime_name, mp4_files if has_files else []
def __remove_year(self, input_string: str):
def __remove_year(self, input_string: str) -> str:
"""Remove year information from input string."""
cleaned_string = re.sub(r'\(\d{4}\)', '', input_string).strip()
logger.debug(
@ -287,7 +292,7 @@ class SerieScanner:
)
return cleaned_string
def __read_data_from_file(self, folder_name: str):
def __read_data_from_file(self, folder_name: str) -> Optional[Serie]:
"""Read serie data from file or key file.
Args:
@ -322,7 +327,7 @@ class SerieScanner:
return None
def __get_episode_and_season(self, filename: str):
def __get_episode_and_season(self, filename: str) -> tuple[int, int]:
"""Extract season and episode numbers from filename.
Args:
@ -355,7 +360,10 @@ class SerieScanner:
"Season and episode pattern not found in the filename."
)
def __get_episodes_and_seasons(self, mp4_files: list):
def __get_episodes_and_seasons(
self,
mp4_files: Iterable[str]
) -> dict[int, list[int]]:
"""Get episodes grouped by season from mp4 files.
Args:
@ -364,7 +372,7 @@ class SerieScanner:
Returns:
Dictionary mapping season to list of episode numbers
"""
episodes_dict = {}
episodes_dict: dict[int, list[int]] = {}
for file in mp4_files:
season, episode = self.__get_episode_and_season(file)
@ -375,7 +383,11 @@ class SerieScanner:
episodes_dict[season] = [episode]
return episodes_dict
def __get_missing_episodes_and_season(self, key: str, mp4_files: list):
def __get_missing_episodes_and_season(
self,
key: str,
mp4_files: Iterable[str]
) -> tuple[dict[int, list[int]], str]:
"""Get missing episodes for a serie.
Args:
@ -388,7 +400,7 @@ class SerieScanner:
# key season , value count of episodes
expected_dict = self.loader.get_season_episode_count(key)
filedict = self.__get_episodes_and_seasons(mp4_files)
episodes_dict = {}
episodes_dict: dict[int, list[int]] = {}
for season, expected_count in expected_dict.items():
existing_episodes = filedict.get(season, [])
missing_episodes = [

View File

@ -27,16 +27,33 @@ noKeyFound_logger = logging.getLogger("NoKeyFound")
noKeyFound_handler = logging.FileHandler("../../NoKeyFound.log")
noKeyFound_handler.setLevel(logging.ERROR)
class AniworldLoader(Loader):
def __init__(self):
self.SUPPORTED_PROVIDERS = ["VOE", "Doodstream", "Vidmoly", "Vidoza", "SpeedFiles", "Streamtape", "Luluvdo"]
self.SUPPORTED_PROVIDERS = [
"VOE",
"Doodstream",
"Vidmoly",
"Vidoza",
"SpeedFiles",
"Streamtape",
"Luluvdo",
]
self.AniworldHeaders = {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"accept": (
"text/html,application/xhtml+xml,application/xml;q=0.9,"
"image/avif,image/webp,image/apng,*/*;q=0.8"
),
"accept-encoding": "gzip, deflate, br, zstd",
"accept-language": "de,de-DE;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"accept-language": (
"de,de-DE;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6"
),
"cache-control": "max-age=0",
"priority": "u=0, i",
"sec-ch-ua": '"Chromium";v="136", "Microsoft Edge";v="136", "Not.A/Brand";v="99"',
"sec-ch-ua": (
'"Chromium";v="136", "Microsoft Edge";v="136", '
'"Not.A/Brand";v="99"'
),
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "document",
@ -44,21 +61,40 @@ class AniworldLoader(Loader):
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"
"user-agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0"
),
}
self.INVALID_PATH_CHARS = ['<', '>', ':', '"', '/', '\\', '|', '?', '*', '&']
self.INVALID_PATH_CHARS = [
"<",
">",
":",
'"',
"/",
"\\",
"|",
"?",
"*",
"&",
]
self.RANDOM_USER_AGENT = UserAgent().random
self.LULUVDO_USER_AGENT = "Mozilla/5.0 (Android 15; Mobile; rv:132.0) Gecko/132.0 Firefox/132.0"
self.LULUVDO_USER_AGENT = (
"Mozilla/5.0 (Android 15; Mobile; rv:132.0) "
"Gecko/132.0 Firefox/132.0"
)
self.PROVIDER_HEADERS = {
"Vidmoly": ['Referer: "https://vidmoly.to"'],
"Doodstream": ['Referer: "https://dood.li/"'],
"VOE": [f'User-Agent: {self.RANDOM_USER_AGENT}'],
"VOE": [f"User-Agent: {self.RANDOM_USER_AGENT}"],
"Luluvdo": [
f'User-Agent: {self.LULUVDO_USER_AGENT}',
'Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7',
f"User-Agent: {self.LULUVDO_USER_AGENT}",
"Accept-Language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7",
'Origin: "https://luluvdo.com"',
'Referer: "https://luluvdo.com/"'
]}
'Referer: "https://luluvdo.com/"',
],
}
self.ANIWORLD_TO = "https://aniworld.to"
self.session = requests.Session()
@ -66,7 +102,7 @@ class AniworldLoader(Loader):
retries = Retry(
total=5, # Number of retries
backoff_factor=1, # Delay multiplier (1s, 2s, 4s, ...)
status_forcelist=[500, 502, 503, 504], # Retry for specific HTTP errors
status_forcelist=[500, 502, 503, 504],
allowed_methods=["GET"]
)
@ -96,12 +132,13 @@ class AniworldLoader(Loader):
Returns:
List of found series
"""
search_url = f"{self.ANIWORLD_TO}/ajax/seriesSearch?keyword={quote(word)}"
search_url = (
f"{self.ANIWORLD_TO}/ajax/seriesSearch?keyword={quote(word)}"
)
anime_list = self.fetch_anime_list(search_url)
return anime_list
def fetch_anime_list(self, url: str) -> list:
response = self.session.get(url, timeout=self.DEFAULT_REQUEST_TIMEOUT)
response.raise_for_status()
@ -297,7 +334,7 @@ class AniworldLoader(Loader):
self._get_episode_html(season, episode, key).content,
'html.parser'
)
providers = {}
providers: dict[str, dict[int, str]] = {}
episode_links = soup.find_all(
'li', class_=lambda x: x and x.startswith('episodeLink')
@ -402,7 +439,10 @@ class AniworldLoader(Loader):
for season in range(1, number_of_seasons + 1):
season_url = f"{base_url}staffel-{season}"
response = requests.get(season_url, timeout=self.DEFAULT_REQUEST_TIMEOUT)
response = requests.get(
season_url,
timeout=self.DEFAULT_REQUEST_TIMEOUT,
)
soup = BeautifulSoup(response.content, 'html.parser')
episode_links = soup.find_all('a', href=True)

View File

@ -1,4 +1,4 @@
from typing import List, Optional
from typing import Any, List, Optional
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
@ -24,40 +24,76 @@ class AnimeDetail(BaseModel):
@router.get("/", response_model=List[AnimeSummary])
async def list_anime(
_auth: dict = Depends(require_auth),
series_app=Depends(get_series_app)
):
"""List series with missing episodes using the core SeriesApp."""
series_app: Any = Depends(get_series_app),
) -> List[AnimeSummary]:
"""List library series that still have missing episodes.
Args:
_auth: Ensures the caller is authenticated (value unused).
series_app: Core `SeriesApp` instance provided via dependency.
Returns:
List[AnimeSummary]: Summary entries describing missing content.
Raises:
HTTPException: When the underlying lookup fails.
"""
try:
series = series_app.List.GetMissingEpisode()
result = []
for s in series:
missing = 0
try:
missing = len(s.episodeDict) if getattr(s, "episodeDict", None) is not None else 0
except Exception:
missing = 0
result.append(AnimeSummary(id=getattr(s, "key", getattr(s, "folder", "")), title=getattr(s, "name", ""), missing_episodes=missing))
return result
summaries: List[AnimeSummary] = []
for serie in series:
episodes_dict = getattr(serie, "episodeDict", {}) or {}
missing_episodes = len(episodes_dict)
key = getattr(serie, "key", getattr(serie, "folder", ""))
title = getattr(serie, "name", "")
summaries.append(
AnimeSummary(
id=key,
title=title,
missing_episodes=missing_episodes,
)
)
return summaries
except HTTPException:
raise
except Exception:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve anime list")
except Exception as exc:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve anime list",
) from exc
@router.post("/rescan")
async def trigger_rescan(series_app=Depends(get_series_app)):
"""Trigger a rescan of local series data using SeriesApp.ReScan."""
async def trigger_rescan(series_app: Any = Depends(get_series_app)) -> dict:
"""Kick off a background rescan of the local library.
Args:
series_app: Core `SeriesApp` instance provided via dependency.
Returns:
Dict[str, Any]: Status payload communicating whether the rescan
launched successfully.
Raises:
HTTPException: If the rescan command is unsupported or fails.
"""
try:
# SeriesApp.ReScan expects a callback; pass a no-op
if hasattr(series_app, "ReScan"):
series_app.ReScan(lambda *args, **kwargs: None)
return {"success": True, "message": "Rescan started"}
else:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Rescan not available")
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Rescan not available",
)
except HTTPException:
raise
except Exception:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to start rescan")
except Exception as exc:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to start rescan",
) from exc
class SearchRequest(BaseModel):
@ -65,56 +101,107 @@ class SearchRequest(BaseModel):
@router.post("/search", response_model=List[AnimeSummary])
async def search_anime(request: SearchRequest, series_app=Depends(get_series_app)):
"""Search for new anime by query text using the SeriesApp loader."""
async def search_anime(
request: SearchRequest,
series_app: Any = Depends(get_series_app),
) -> List[AnimeSummary]:
"""Search the provider for additional series matching a query.
Args:
request: Incoming payload containing the search term.
series_app: Core `SeriesApp` instance provided via dependency.
Returns:
List[AnimeSummary]: Discovered matches returned from the provider.
Raises:
HTTPException: When provider communication fails.
"""
try:
matches = []
matches: List[Any] = []
if hasattr(series_app, "search"):
# SeriesApp.search is synchronous in core; call directly
matches = series_app.search(request.query)
result = []
for m in matches:
# matches may be dicts or objects
if isinstance(m, dict):
mid = m.get("key") or m.get("id") or ""
title = m.get("title") or m.get("name") or ""
missing = int(m.get("missing", 0)) if m.get("missing") is not None else 0
summaries: List[AnimeSummary] = []
for match in matches:
if isinstance(match, dict):
identifier = match.get("key") or match.get("id") or ""
title = match.get("title") or match.get("name") or ""
missing = match.get("missing")
missing_episodes = int(missing) if missing is not None else 0
else:
mid = getattr(m, "key", getattr(m, "id", ""))
title = getattr(m, "title", getattr(m, "name", ""))
missing = int(getattr(m, "missing", 0))
result.append(AnimeSummary(id=mid, title=title, missing_episodes=missing))
identifier = getattr(match, "key", getattr(match, "id", ""))
title = getattr(match, "title", getattr(match, "name", ""))
missing_episodes = int(getattr(match, "missing", 0))
return result
summaries.append(
AnimeSummary(
id=identifier,
title=title,
missing_episodes=missing_episodes,
)
)
return summaries
except HTTPException:
raise
except Exception:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Search failed")
except Exception as exc:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Search failed",
) from exc
@router.get("/{anime_id}", response_model=AnimeDetail)
async def get_anime(anime_id: str, series_app=Depends(get_series_app)):
"""Return detailed info about a series from SeriesApp.List."""
async def get_anime(
anime_id: str,
series_app: Any = Depends(get_series_app)
) -> AnimeDetail:
"""Return detailed information about a specific series.
Args:
anime_id: Provider key or folder name of the requested series.
series_app: Core `SeriesApp` instance provided via dependency.
Returns:
AnimeDetail: Detailed series metadata including episode list.
Raises:
HTTPException: If the anime cannot be located or retrieval fails.
"""
try:
series = series_app.List.GetList()
found = None
for s in series:
if getattr(s, "key", None) == anime_id or getattr(s, "folder", None) == anime_id:
found = s
for serie in series:
matches_key = getattr(serie, "key", None) == anime_id
matches_folder = getattr(serie, "folder", None) == anime_id
if matches_key or matches_folder:
found = serie
break
if not found:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Series not found")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Series not found",
)
episodes = []
epdict = getattr(found, "episodeDict", {}) or {}
for season, eps in epdict.items():
for e in eps:
episodes.append(f"{season}-{e}")
episodes: List[str] = []
episode_dict = getattr(found, "episodeDict", {}) or {}
for season, episode_numbers in episode_dict.items():
for episode in episode_numbers:
episodes.append(f"{season}-{episode}")
return AnimeDetail(id=getattr(found, "key", getattr(found, "folder", "")), title=getattr(found, "name", ""), episodes=episodes, description=getattr(found, "description", None))
return AnimeDetail(
id=getattr(found, "key", getattr(found, "folder", "")),
title=getattr(found, "name", ""),
episodes=episodes,
description=getattr(found, "description", None),
)
except HTTPException:
raise
except Exception:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve series details")
except Exception as exc:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve series details",
) from exc

View File

@ -5,7 +5,7 @@ This module provides dependency injection functions for the FastAPI
application, including SeriesApp instances, AnimeService, DownloadService,
database sessions, and authentication dependencies.
"""
from typing import AsyncGenerator, Optional
from typing import TYPE_CHECKING, AsyncGenerator, Optional
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
@ -19,6 +19,10 @@ from src.config.settings import settings
from src.core.SeriesApp import SeriesApp
from src.server.services.auth_service import AuthError, auth_service
if TYPE_CHECKING:
from src.server.services.anime_service import AnimeService
from src.server.services.download_service import DownloadService
# Security scheme for JWT authentication
# Use auto_error=False to handle errors manually and return 401 instead of 403
security = HTTPBearer(auto_error=False)
@ -28,8 +32,8 @@ security = HTTPBearer(auto_error=False)
_series_app: Optional[SeriesApp] = None
# Global service instances
_anime_service: Optional[object] = None
_download_service: Optional[object] = None
_anime_service: Optional["AnimeService"] = None
_download_service: Optional["DownloadService"] = None
def get_series_app() -> SeriesApp:
@ -193,7 +197,13 @@ def get_current_user_optional(
class CommonQueryParams:
"""Common query parameters for API endpoints."""
def __init__(self, skip: int = 0, limit: int = 100):
def __init__(self, skip: int = 0, limit: int = 100) -> None:
"""Create a reusable pagination parameter container.
Args:
skip: Number of records to offset when querying collections.
limit: Maximum number of records to return in a single call.
"""
self.skip = skip
self.limit = limit
@ -235,7 +245,7 @@ async def log_request_dependency():
pass
def get_anime_service() -> object:
def get_anime_service() -> "AnimeService":
"""
Dependency to get AnimeService instance.
@ -257,29 +267,39 @@ def get_anime_service() -> object:
import sys
import tempfile
running_tests = "PYTEST_CURRENT_TEST" in os.environ or "pytest" in sys.modules
running_tests = (
"PYTEST_CURRENT_TEST" in os.environ
or "pytest" in sys.modules
)
if running_tests:
settings.anime_directory = tempfile.gettempdir()
else:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Anime directory not configured. Please complete setup.",
detail=(
"Anime directory not configured. "
"Please complete setup."
),
)
if _anime_service is None:
try:
from src.server.services.anime_service import AnimeService
_anime_service = AnimeService(settings.anime_directory)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to initialize AnimeService: {str(e)}",
detail=(
"Failed to initialize AnimeService: "
f"{str(e)}"
),
) from e
return _anime_service
def get_download_service() -> object:
def get_download_service() -> "DownloadService":
"""
Dependency to get DownloadService instance.
@ -293,36 +313,36 @@ def get_download_service() -> object:
if _download_service is None:
try:
from src.server.services import (
websocket_service as websocket_service_module,
)
from src.server.services.download_service import DownloadService
from src.server.services.websocket_service import get_websocket_service
# Get anime service first (required dependency)
anime_service = get_anime_service()
# Initialize download service with anime service
_download_service = DownloadService(anime_service)
# Setup WebSocket broadcast callback
ws_service = get_websocket_service()
ws_service = websocket_service_module.get_websocket_service()
async def broadcast_callback(update_type: str, data: dict):
async def broadcast_callback(update_type: str, data: dict) -> None:
"""Broadcast download updates via WebSocket."""
if update_type == "download_progress":
await ws_service.broadcast_download_progress(
data.get("download_id", ""), data
data.get("download_id", ""),
data,
)
elif update_type == "download_complete":
await ws_service.broadcast_download_complete(
data.get("download_id", ""), data
data.get("download_id", ""),
data,
)
elif update_type == "download_failed":
await ws_service.broadcast_download_failed(
data.get("download_id", ""), data
data.get("download_id", ""),
data,
)
elif update_type == "queue_status":
await ws_service.broadcast_queue_status(data)
else:
# Generic queue update
await ws_service.broadcast_queue_status(data)
_download_service.set_broadcast_callback(broadcast_callback)
@ -332,7 +352,10 @@ def get_download_service() -> object:
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to initialize DownloadService: {str(e)}",
detail=(
"Failed to initialize DownloadService: "
f"{str(e)}"
),
) from e
return _download_service