backup
This commit is contained in:
parent
32dc893434
commit
9b071fe370
BIN
data/aniworld.db-shm
Normal file
BIN
data/aniworld.db-shm
Normal file
Binary file not shown.
BIN
data/aniworld.db-wal
Normal file
BIN
data/aniworld.db-wal
Normal file
Binary file not shown.
@ -16,6 +16,9 @@
|
|||||||
"path": "data/backups",
|
"path": "data/backups",
|
||||||
"keep_days": 30
|
"keep_days": 30
|
||||||
},
|
},
|
||||||
"other": {},
|
"other": {
|
||||||
|
"master_password_hash": "$pbkdf2-sha256$29000$hdC6t/b.f885Z0xprfXeWw$7K3TmeKN2jtTZq8/xiQjm3Y5DCLx8s0Nj9mIZbs/XUM",
|
||||||
|
"anime_directory": "/mnt/server/serien/Serien/"
|
||||||
|
},
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
}
|
}
|
||||||
@ -120,3 +120,148 @@ For each task completed:
|
|||||||
- Good foundation for future enhancements if needed
|
- Good foundation for future enhancements if needed
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🔧 Current Task: Make MP4 Scanning Progress Visible in UI
|
||||||
|
|
||||||
|
### Problem Statement
|
||||||
|
|
||||||
|
When users trigger a library rescan (via the "Rescan Library" button on the anime page), the MP4 file scanning happens silently in the background. Users only see a brief toast message, but there's no visual feedback showing:
|
||||||
|
|
||||||
|
1. That scanning is actively happening
|
||||||
|
2. How many files/directories have been scanned
|
||||||
|
3. The progress through the scan operation
|
||||||
|
4. When scanning is complete with results
|
||||||
|
|
||||||
|
Currently, the only indication is in server logs:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO: Starting directory rescan
|
||||||
|
INFO: Scanning for .mp4 files
|
||||||
|
```
|
||||||
|
|
||||||
|
### Desired Outcome
|
||||||
|
|
||||||
|
Users should see real-time progress in the UI during library scanning with:
|
||||||
|
|
||||||
|
1. **Progress overlay** showing scan is active with a spinner animation
|
||||||
|
2. **Live counters** showing directories scanned and files found
|
||||||
|
3. **Current directory display** showing which folder is being scanned (truncated if too long)
|
||||||
|
4. **Completion summary** showing total files found, directories scanned, and elapsed time
|
||||||
|
5. **Auto-dismiss** the overlay after showing completion summary
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
|
||||||
|
#### 1. `src/server/services/websocket_service.py`
|
||||||
|
|
||||||
|
Add three new broadcast methods for scan events:
|
||||||
|
|
||||||
|
- **broadcast_scan_started**: Notify clients that a scan has begun, include the root directory path
|
||||||
|
- **broadcast_scan_progress**: Send periodic updates with directories scanned count, files found count, and current directory name
|
||||||
|
- **broadcast_scan_completed**: Send final summary with total directories, total files, and elapsed time in seconds
|
||||||
|
|
||||||
|
Follow the existing pattern used by `broadcast_download_progress` for message structure consistency.
|
||||||
|
|
||||||
|
#### 2. `src/server/services/scanner_service.py`
|
||||||
|
|
||||||
|
Modify the scanning logic to emit progress via WebSocket:
|
||||||
|
|
||||||
|
- Inject `WebSocketService` dependency into the scanner service
|
||||||
|
- At scan start, call `broadcast_scan_started`
|
||||||
|
- During directory traversal, track directories scanned and files found
|
||||||
|
- Every 10 directories (to avoid WebSocket spam), call `broadcast_scan_progress`
|
||||||
|
- Track elapsed time using `time.time()`
|
||||||
|
- At scan completion, call `broadcast_scan_completed` with summary statistics
|
||||||
|
- Ensure the scan still works correctly even if WebSocket broadcast fails (wrap in try/except)
|
||||||
|
|
||||||
|
#### 3. `src/server/static/css/style.css`
|
||||||
|
|
||||||
|
Add styles for the scan progress overlay:
|
||||||
|
|
||||||
|
- Full-screen semi-transparent overlay (z-index high enough to be on top)
|
||||||
|
- Centered container with background matching theme (use CSS variables)
|
||||||
|
- Spinner animation using CSS keyframes
|
||||||
|
- Styling for current directory text (truncated with ellipsis)
|
||||||
|
- Styling for statistics display
|
||||||
|
- Success state styling for completion
|
||||||
|
- Ensure it works in both light and dark mode themes
|
||||||
|
|
||||||
|
#### 4. `src/server/static/js/anime.js`
|
||||||
|
|
||||||
|
Add WebSocket message handlers and UI functions:
|
||||||
|
|
||||||
|
- Handle `scan_started` message: Create and show progress overlay with spinner
|
||||||
|
- Handle `scan_progress` message: Update directory count, file count, and current directory text
|
||||||
|
- Handle `scan_completed` message: Show completion summary, then auto-remove overlay after 3 seconds
|
||||||
|
- Ensure overlay is properly cleaned up if page navigates away
|
||||||
|
- Update the existing rescan button handler to work with the new progress system
|
||||||
|
|
||||||
|
### WebSocket Message Types
|
||||||
|
|
||||||
|
Define three new message types following the existing project patterns:
|
||||||
|
|
||||||
|
1. **scan_started**: type, directory path, timestamp
|
||||||
|
2. **scan_progress**: type, directories_scanned, files_found, current_directory, timestamp
|
||||||
|
3. **scan_completed**: type, total_directories, total_files, elapsed_seconds, timestamp
|
||||||
|
|
||||||
|
### Implementation Steps
|
||||||
|
|
||||||
|
1. First modify `websocket_service.py` to add the three new broadcast methods
|
||||||
|
2. Add unit tests for the new WebSocket methods
|
||||||
|
3. Modify `scanner_service.py` to use the new broadcast methods during scanning
|
||||||
|
4. Add CSS styles to `style.css` for the progress overlay
|
||||||
|
5. Update `anime.js` to handle the new WebSocket messages and display the UI
|
||||||
|
6. Test the complete flow manually
|
||||||
|
7. Verify all existing tests still pass
|
||||||
|
|
||||||
|
### Testing Requirements
|
||||||
|
|
||||||
|
**Unit Tests:**
|
||||||
|
|
||||||
|
- Test each new WebSocket broadcast method
|
||||||
|
- Test that scanner service calls WebSocket methods at appropriate times
|
||||||
|
- Mock WebSocket service in scanner tests
|
||||||
|
|
||||||
|
**Manual Testing:**
|
||||||
|
|
||||||
|
- Start server and login
|
||||||
|
- Navigate to anime page
|
||||||
|
- Click "Rescan Library" button
|
||||||
|
- Verify overlay appears immediately with spinner
|
||||||
|
- Verify counters update during scan
|
||||||
|
- Verify current directory updates
|
||||||
|
- Verify completion summary appears
|
||||||
|
- Verify overlay auto-dismisses after 3 seconds
|
||||||
|
- Test in both light and dark mode
|
||||||
|
- Verify no JavaScript console errors
|
||||||
|
|
||||||
|
### Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] Progress overlay appears immediately when scan starts
|
||||||
|
- [ ] Spinner animation is visible during scanning
|
||||||
|
- [ ] Directory counter updates periodically (every ~10 directories)
|
||||||
|
- [ ] Files found counter updates as MP4 files are discovered
|
||||||
|
- [ ] Current directory name is displayed (truncated if path is too long)
|
||||||
|
- [ ] Scan completion shows total directories, files, and elapsed time
|
||||||
|
- [ ] Overlay auto-dismisses 3 seconds after completion
|
||||||
|
- [ ] Works correctly in both light and dark mode
|
||||||
|
- [ ] No JavaScript errors in browser console
|
||||||
|
- [ ] All existing tests continue to pass
|
||||||
|
- [ ] New unit tests added and passing
|
||||||
|
- [ ] Code follows project coding standards
|
||||||
|
|
||||||
|
### Edge Cases to Handle
|
||||||
|
|
||||||
|
- Empty directory with no MP4 files
|
||||||
|
- Very large directory structure (ensure UI remains responsive)
|
||||||
|
- WebSocket connection lost during scan (scan should still complete)
|
||||||
|
- User navigates away during scan (cleanup overlay properly)
|
||||||
|
- Rapid consecutive scan requests (debounce or queue)
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- Keep progress updates throttled to avoid overwhelming the WebSocket connection
|
||||||
|
- Use existing CSS variables for colors to maintain theme consistency
|
||||||
|
- Follow existing JavaScript patterns in the codebase
|
||||||
|
- The scan functionality must continue to work even if WebSocket fails
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@ -81,6 +81,7 @@ class AnimeSummary(BaseModel):
|
|||||||
site: Provider site URL
|
site: Provider site URL
|
||||||
folder: Filesystem folder name (metadata only)
|
folder: Filesystem folder name (metadata only)
|
||||||
missing_episodes: Episode dictionary mapping seasons to episode numbers
|
missing_episodes: Episode dictionary mapping seasons to episode numbers
|
||||||
|
has_missing: Boolean flag indicating if series has missing episodes
|
||||||
link: Optional link to the series page (used when adding new series)
|
link: Optional link to the series page (used when adding new series)
|
||||||
"""
|
"""
|
||||||
key: str = Field(
|
key: str = Field(
|
||||||
@ -103,6 +104,10 @@ class AnimeSummary(BaseModel):
|
|||||||
...,
|
...,
|
||||||
description="Episode dictionary: {season: [episode_numbers]}"
|
description="Episode dictionary: {season: [episode_numbers]}"
|
||||||
)
|
)
|
||||||
|
has_missing: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description="Whether the series has any missing episodes"
|
||||||
|
)
|
||||||
link: Optional[str] = Field(
|
link: Optional[str] = Field(
|
||||||
default="",
|
default="",
|
||||||
description="Link to the series page (for adding new series)"
|
description="Link to the series page (for adding new series)"
|
||||||
@ -117,6 +122,7 @@ class AnimeSummary(BaseModel):
|
|||||||
"site": "aniworld.to",
|
"site": "aniworld.to",
|
||||||
"folder": "beheneko the elf girls cat (2025)",
|
"folder": "beheneko the elf girls cat (2025)",
|
||||||
"missing_episodes": {"1": [1, 2, 3, 4]},
|
"missing_episodes": {"1": [1, 2, 3, 4]},
|
||||||
|
"has_missing": True,
|
||||||
"link": "https://aniworld.to/anime/stream/beheneko"
|
"link": "https://aniworld.to/anime/stream/beheneko"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -181,12 +187,15 @@ async def list_anime(
|
|||||||
_auth: dict = Depends(require_auth),
|
_auth: dict = Depends(require_auth),
|
||||||
series_app: Any = Depends(get_series_app),
|
series_app: Any = Depends(get_series_app),
|
||||||
) -> List[AnimeSummary]:
|
) -> List[AnimeSummary]:
|
||||||
"""List library series that still have missing episodes.
|
"""List all library series with their missing episodes status.
|
||||||
|
|
||||||
Returns AnimeSummary objects where `key` is the primary identifier
|
Returns AnimeSummary objects where `key` is the primary identifier
|
||||||
used for all operations. The `folder` field is metadata only and
|
used for all operations. The `folder` field is metadata only and
|
||||||
should not be used for lookups.
|
should not be used for lookups.
|
||||||
|
|
||||||
|
All series are returned, with `has_missing` flag indicating whether
|
||||||
|
a series has any missing episodes.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
page: Page number for pagination (must be positive)
|
page: Page number for pagination (must be positive)
|
||||||
per_page: Items per page (must be positive, max 1000)
|
per_page: Items per page (must be positive, max 1000)
|
||||||
@ -204,6 +213,7 @@ async def list_anime(
|
|||||||
- site: Provider site
|
- site: Provider site
|
||||||
- folder: Filesystem folder name (metadata only)
|
- folder: Filesystem folder name (metadata only)
|
||||||
- missing_episodes: Dict mapping seasons to episode numbers
|
- missing_episodes: Dict mapping seasons to episode numbers
|
||||||
|
- has_missing: Whether the series has any missing episodes
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPException: When the underlying lookup fails or params invalid.
|
HTTPException: When the underlying lookup fails or params invalid.
|
||||||
@ -264,11 +274,11 @@ async def list_anime(
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get missing episodes from series app
|
# Get all series from series app
|
||||||
if not hasattr(series_app, "list"):
|
if not hasattr(series_app, "list"):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
series = series_app.list.GetMissingEpisode()
|
series = series_app.list.GetList()
|
||||||
summaries: List[AnimeSummary] = []
|
summaries: List[AnimeSummary] = []
|
||||||
for serie in series:
|
for serie in series:
|
||||||
# Get all properties from the serie object
|
# Get all properties from the serie object
|
||||||
@ -281,6 +291,9 @@ async def list_anime(
|
|||||||
# Convert episode dict keys to strings for JSON serialization
|
# Convert episode dict keys to strings for JSON serialization
|
||||||
missing_episodes = {str(k): v for k, v in episode_dict.items()}
|
missing_episodes = {str(k): v for k, v in episode_dict.items()}
|
||||||
|
|
||||||
|
# Determine if series has missing episodes
|
||||||
|
has_missing = bool(episode_dict)
|
||||||
|
|
||||||
summaries.append(
|
summaries.append(
|
||||||
AnimeSummary(
|
AnimeSummary(
|
||||||
key=key,
|
key=key,
|
||||||
@ -288,6 +301,7 @@ async def list_anime(
|
|||||||
site=site,
|
site=site,
|
||||||
folder=folder,
|
folder=folder,
|
||||||
missing_episodes=missing_episodes,
|
missing_episodes=missing_episodes,
|
||||||
|
has_missing=has_missing,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -565,7 +565,8 @@ class AniWorldApp {
|
|||||||
site: anime.site,
|
site: anime.site,
|
||||||
folder: anime.folder,
|
folder: anime.folder,
|
||||||
episodeDict: episodeDict,
|
episodeDict: episodeDict,
|
||||||
missing_episodes: totalMissing
|
missing_episodes: totalMissing,
|
||||||
|
has_missing: anime.has_missing || totalMissing > 0
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} else if (data.status === 'success') {
|
} else if (data.status === 'success') {
|
||||||
|
|||||||
@ -162,6 +162,7 @@ class TestFrontendAuthentication:
|
|||||||
mock_app = AsyncMock()
|
mock_app = AsyncMock()
|
||||||
mock_list = AsyncMock()
|
mock_list = AsyncMock()
|
||||||
mock_list.GetMissingEpisode = AsyncMock(return_value=[])
|
mock_list.GetMissingEpisode = AsyncMock(return_value=[])
|
||||||
|
mock_list.GetList = AsyncMock(return_value=[])
|
||||||
mock_app.List = mock_list
|
mock_app.List = mock_list
|
||||||
mock_get_app.return_value = mock_app
|
mock_get_app.return_value = mock_app
|
||||||
|
|
||||||
|
|||||||
@ -29,6 +29,7 @@ class TestAPILoadTesting:
|
|||||||
mock_app = MagicMock()
|
mock_app = MagicMock()
|
||||||
mock_app.list = MagicMock()
|
mock_app.list = MagicMock()
|
||||||
mock_app.list.GetMissingEpisode = MagicMock(return_value=[])
|
mock_app.list.GetMissingEpisode = MagicMock(return_value=[])
|
||||||
|
mock_app.list.GetList = MagicMock(return_value=[])
|
||||||
mock_app.search = AsyncMock(return_value=[])
|
mock_app.search = AsyncMock(return_value=[])
|
||||||
|
|
||||||
app.dependency_overrides[get_series_app] = lambda: mock_app
|
app.dependency_overrides[get_series_app] = lambda: mock_app
|
||||||
|
|||||||
@ -27,6 +27,7 @@ class TestSQLInjection:
|
|||||||
mock_app = MagicMock()
|
mock_app = MagicMock()
|
||||||
mock_app.list = MagicMock()
|
mock_app.list = MagicMock()
|
||||||
mock_app.list.GetMissingEpisode = MagicMock(return_value=[])
|
mock_app.list.GetMissingEpisode = MagicMock(return_value=[])
|
||||||
|
mock_app.list.GetList = MagicMock(return_value=[])
|
||||||
mock_app.search = AsyncMock(return_value=[])
|
mock_app.search = AsyncMock(return_value=[])
|
||||||
|
|
||||||
# Override dependency
|
# Override dependency
|
||||||
@ -287,6 +288,7 @@ class TestDatabaseSecurity:
|
|||||||
mock_app = MagicMock()
|
mock_app = MagicMock()
|
||||||
mock_app.list = MagicMock()
|
mock_app.list = MagicMock()
|
||||||
mock_app.list.GetMissingEpisode = MagicMock(return_value=[])
|
mock_app.list.GetMissingEpisode = MagicMock(return_value=[])
|
||||||
|
mock_app.list.GetList = MagicMock(return_value=[])
|
||||||
mock_app.search = AsyncMock(return_value=[])
|
mock_app.search = AsyncMock(return_value=[])
|
||||||
|
|
||||||
# Override dependency
|
# Override dependency
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user