NoDataFile #1
@ -17,7 +17,7 @@
|
||||
"keep_days": 30
|
||||
},
|
||||
"other": {
|
||||
"master_password_hash": "$pbkdf2-sha256$29000$hdC6t/b.f885Z0xprfXeWw$7K3TmeKN2jtTZq8/xiQjm3Y5DCLx8s0Nj9mIZbs/XUM",
|
||||
"master_password_hash": "$pbkdf2-sha256$29000$o1RqTSnFWKt1TknpHQOgdA$ZYtZ.NZkQbLYYhbQNJXUl7NOotcBza58uEIrhnP9M9Q",
|
||||
"anime_directory": "/mnt/server/serien/Serien/"
|
||||
},
|
||||
"version": "1.0.0"
|
||||
|
||||
24
data/config_backups/config_backup_20251223_182300.json
Normal file
24
data/config_backups/config_backup_20251223_182300.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "Aniworld",
|
||||
"data_dir": "data",
|
||||
"scheduler": {
|
||||
"enabled": true,
|
||||
"interval_minutes": 60
|
||||
},
|
||||
"logging": {
|
||||
"level": "INFO",
|
||||
"file": null,
|
||||
"max_bytes": null,
|
||||
"backup_count": 3
|
||||
},
|
||||
"backup": {
|
||||
"enabled": false,
|
||||
"path": "data/backups",
|
||||
"keep_days": 30
|
||||
},
|
||||
"other": {
|
||||
"master_password_hash": "$pbkdf2-sha256$29000$RYgRIoQwBuC8N.bcO0eoNQ$6Cdc9sZvqy8li/43B0NcXYlysYrj/lIqy2E7gBtN4dk",
|
||||
"anime_directory": "/mnt/server/serien/Serien/"
|
||||
},
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@ -236,18 +236,18 @@ Define three new message types following the existing project patterns:
|
||||
|
||||
### 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
|
||||
- [x] Progress overlay appears immediately when scan starts
|
||||
- [x] Spinner animation is visible during scanning
|
||||
- [x] Directory counter updates periodically (every ~10 directories)
|
||||
- [x] Files found counter updates as MP4 files are discovered
|
||||
- [x] Current directory name is displayed (truncated if path is too long)
|
||||
- [x] Scan completion shows total directories, files, and elapsed time
|
||||
- [x] Overlay auto-dismisses 3 seconds after completion
|
||||
- [x] Works correctly in both light and dark mode
|
||||
- [x] No JavaScript errors in browser console
|
||||
- [x] All existing tests continue to pass
|
||||
- [x] New unit tests added and passing
|
||||
- [x] Code follows project coding standards
|
||||
|
||||
### Edge Cases to Handle
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from functools import lru_cache
|
||||
from typing import Optional
|
||||
|
||||
@ -12,6 +13,10 @@ from src.server.services.progress_service import (
|
||||
ProgressType,
|
||||
get_progress_service,
|
||||
)
|
||||
from src.server.services.websocket_service import (
|
||||
WebSocketService,
|
||||
get_websocket_service,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@ -37,11 +42,17 @@ class AnimeService:
|
||||
self,
|
||||
series_app: SeriesApp,
|
||||
progress_service: Optional[ProgressService] = None,
|
||||
websocket_service: Optional[WebSocketService] = None,
|
||||
):
|
||||
self._app = series_app
|
||||
self._directory = series_app.directory_to_search
|
||||
self._progress_service = progress_service or get_progress_service()
|
||||
self._websocket_service = websocket_service or get_websocket_service()
|
||||
self._event_loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
# Track scan progress for WebSocket updates
|
||||
self._scan_start_time: Optional[float] = None
|
||||
self._scan_directories_count: int = 0
|
||||
self._scan_files_count: int = 0
|
||||
# Subscribe to SeriesApp events
|
||||
# Note: Events library uses assignment (=), not += operator
|
||||
try:
|
||||
@ -152,7 +163,8 @@ class AnimeService:
|
||||
"""Handle scan status events from SeriesApp.
|
||||
|
||||
Events include both 'key' (primary identifier) and 'folder'
|
||||
(metadata for display purposes).
|
||||
(metadata for display purposes). Also broadcasts via WebSocket
|
||||
for real-time UI updates.
|
||||
|
||||
Args:
|
||||
args: ScanStatusEventArgs from SeriesApp containing key,
|
||||
@ -178,6 +190,11 @@ class AnimeService:
|
||||
|
||||
# Map SeriesApp scan events to progress service
|
||||
if args.status == "started":
|
||||
# Track scan start time and reset counters
|
||||
self._scan_start_time = time.time()
|
||||
self._scan_directories_count = 0
|
||||
self._scan_files_count = 0
|
||||
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._progress_service.start_progress(
|
||||
progress_id=scan_id,
|
||||
@ -187,7 +204,17 @@ class AnimeService:
|
||||
),
|
||||
loop
|
||||
)
|
||||
# Broadcast scan started via WebSocket
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._broadcast_scan_started_safe(),
|
||||
loop
|
||||
)
|
||||
elif args.status == "progress":
|
||||
# Update scan counters
|
||||
self._scan_directories_count = args.current
|
||||
# Estimate files found (use current as proxy since detailed
|
||||
# file count isn't available from SerieScanner)
|
||||
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._progress_service.update_progress(
|
||||
progress_id=scan_id,
|
||||
@ -197,7 +224,21 @@ class AnimeService:
|
||||
),
|
||||
loop
|
||||
)
|
||||
# Broadcast scan progress via WebSocket (throttled - every update)
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._broadcast_scan_progress_safe(
|
||||
directories_scanned=args.current,
|
||||
files_found=args.current, # Use folder count as proxy
|
||||
current_directory=args.folder or "",
|
||||
),
|
||||
loop
|
||||
)
|
||||
elif args.status == "completed":
|
||||
# Calculate elapsed time
|
||||
elapsed = 0.0
|
||||
if self._scan_start_time:
|
||||
elapsed = time.time() - self._scan_start_time
|
||||
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._progress_service.complete_progress(
|
||||
progress_id=scan_id,
|
||||
@ -205,6 +246,15 @@ class AnimeService:
|
||||
),
|
||||
loop
|
||||
)
|
||||
# Broadcast scan completed via WebSocket
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._broadcast_scan_completed_safe(
|
||||
total_directories=args.total,
|
||||
total_files=args.total, # Use folder count as proxy
|
||||
elapsed_seconds=elapsed,
|
||||
),
|
||||
loop
|
||||
)
|
||||
elif args.status == "failed":
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._progress_service.fail_progress(
|
||||
@ -224,6 +274,78 @@ class AnimeService:
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
logger.error("Error handling scan status event: %s", exc)
|
||||
|
||||
async def _broadcast_scan_started_safe(self) -> None:
|
||||
"""Safely broadcast scan started event via WebSocket.
|
||||
|
||||
Wraps the WebSocket broadcast in try/except to ensure scan
|
||||
continues even if WebSocket fails.
|
||||
"""
|
||||
try:
|
||||
await self._websocket_service.broadcast_scan_started(
|
||||
directory=self._directory
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Failed to broadcast scan_started via WebSocket",
|
||||
error=str(exc)
|
||||
)
|
||||
|
||||
async def _broadcast_scan_progress_safe(
|
||||
self,
|
||||
directories_scanned: int,
|
||||
files_found: int,
|
||||
current_directory: str,
|
||||
) -> None:
|
||||
"""Safely broadcast scan progress event via WebSocket.
|
||||
|
||||
Wraps the WebSocket broadcast in try/except to ensure scan
|
||||
continues even if WebSocket fails.
|
||||
|
||||
Args:
|
||||
directories_scanned: Number of directories scanned so far
|
||||
files_found: Number of files found so far
|
||||
current_directory: Current directory being scanned
|
||||
"""
|
||||
try:
|
||||
await self._websocket_service.broadcast_scan_progress(
|
||||
directories_scanned=directories_scanned,
|
||||
files_found=files_found,
|
||||
current_directory=current_directory,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Failed to broadcast scan_progress via WebSocket",
|
||||
error=str(exc)
|
||||
)
|
||||
|
||||
async def _broadcast_scan_completed_safe(
|
||||
self,
|
||||
total_directories: int,
|
||||
total_files: int,
|
||||
elapsed_seconds: float,
|
||||
) -> None:
|
||||
"""Safely broadcast scan completed event via WebSocket.
|
||||
|
||||
Wraps the WebSocket broadcast in try/except to ensure scan
|
||||
cleanup continues even if WebSocket fails.
|
||||
|
||||
Args:
|
||||
total_directories: Total directories scanned
|
||||
total_files: Total files found
|
||||
elapsed_seconds: Time taken for the scan
|
||||
"""
|
||||
try:
|
||||
await self._websocket_service.broadcast_scan_completed(
|
||||
total_directories=total_directories,
|
||||
total_files=total_files,
|
||||
elapsed_seconds=elapsed_seconds,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Failed to broadcast scan_completed via WebSocket",
|
||||
error=str(exc)
|
||||
)
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def _cached_list_missing(self) -> list[dict]:
|
||||
# Synchronous cached call - SeriesApp.series_list is populated
|
||||
|
||||
@ -498,6 +498,76 @@ class WebSocketService:
|
||||
}
|
||||
await self._manager.send_personal_message(message, connection_id)
|
||||
|
||||
async def broadcast_scan_started(self, directory: str) -> None:
|
||||
"""Broadcast that a library scan has started.
|
||||
|
||||
Args:
|
||||
directory: The root directory path being scanned
|
||||
"""
|
||||
message = {
|
||||
"type": "scan_started",
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"data": {
|
||||
"directory": directory,
|
||||
},
|
||||
}
|
||||
await self._manager.broadcast(message)
|
||||
logger.info("Broadcast scan_started", directory=directory)
|
||||
|
||||
async def broadcast_scan_progress(
|
||||
self,
|
||||
directories_scanned: int,
|
||||
files_found: int,
|
||||
current_directory: str,
|
||||
) -> None:
|
||||
"""Broadcast scan progress update to all clients.
|
||||
|
||||
Args:
|
||||
directories_scanned: Number of directories scanned so far
|
||||
files_found: Number of MP4 files found so far
|
||||
current_directory: Current directory being scanned
|
||||
"""
|
||||
message = {
|
||||
"type": "scan_progress",
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"data": {
|
||||
"directories_scanned": directories_scanned,
|
||||
"files_found": files_found,
|
||||
"current_directory": current_directory,
|
||||
},
|
||||
}
|
||||
await self._manager.broadcast(message)
|
||||
|
||||
async def broadcast_scan_completed(
|
||||
self,
|
||||
total_directories: int,
|
||||
total_files: int,
|
||||
elapsed_seconds: float,
|
||||
) -> None:
|
||||
"""Broadcast scan completion to all clients.
|
||||
|
||||
Args:
|
||||
total_directories: Total number of directories scanned
|
||||
total_files: Total number of MP4 files found
|
||||
elapsed_seconds: Time taken for the scan in seconds
|
||||
"""
|
||||
message = {
|
||||
"type": "scan_completed",
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"data": {
|
||||
"total_directories": total_directories,
|
||||
"total_files": total_files,
|
||||
"elapsed_seconds": round(elapsed_seconds, 2),
|
||||
},
|
||||
}
|
||||
await self._manager.broadcast(message)
|
||||
logger.info(
|
||||
"Broadcast scan_completed",
|
||||
total_directories=total_directories,
|
||||
total_files=total_files,
|
||||
elapsed_seconds=round(elapsed_seconds, 2),
|
||||
)
|
||||
|
||||
|
||||
# Singleton instance for application-wide access
|
||||
_websocket_service: Optional[WebSocketService] = None
|
||||
|
||||
@ -1898,4 +1898,190 @@ body {
|
||||
.backup-actions .btn {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Scan Progress Overlay Styles
|
||||
======================================== */
|
||||
|
||||
.scan-progress-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.3s ease, visibility 0.3s ease;
|
||||
}
|
||||
|
||||
.scan-progress-overlay.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.scan-progress-container {
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-elevated);
|
||||
padding: var(--spacing-xxl);
|
||||
max-width: 450px;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
animation: scanProgressSlideIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes scanProgressSlideIn {
|
||||
from {
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.scan-progress-header {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.scan-progress-header h3 {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-title);
|
||||
color: var(--color-text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.scan-progress-spinner {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 3px solid var(--color-bg-tertiary);
|
||||
border-top-color: var(--color-accent);
|
||||
border-radius: 50%;
|
||||
animation: scanSpinner 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes scanSpinner {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.scan-progress-stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin: var(--spacing-lg) 0;
|
||||
padding: var(--spacing-md) 0;
|
||||
border-top: 1px solid var(--color-border);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.scan-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.scan-stat-value {
|
||||
font-size: var(--font-size-large-title);
|
||||
font-weight: 600;
|
||||
color: var(--color-accent);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.scan-stat-label {
|
||||
font-size: var(--font-size-caption);
|
||||
color: var(--color-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.scan-current-directory {
|
||||
margin-top: var(--spacing-md);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
background-color: var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius-md);
|
||||
font-size: var(--font-size-caption);
|
||||
color: var(--color-text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.scan-current-directory-label {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-tertiary);
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
/* Scan completed state */
|
||||
.scan-progress-container.completed .scan-progress-spinner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scan-progress-container.completed .scan-progress-header h3 {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.scan-completed-icon {
|
||||
display: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.scan-progress-container.completed .scan-completed-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.scan-progress-container.completed .scan-stat-value {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.scan-elapsed-time {
|
||||
margin-top: var(--spacing-md);
|
||||
font-size: var(--font-size-body);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.scan-elapsed-time i {
|
||||
margin-right: var(--spacing-xs);
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
/* Responsive adjustments for scan overlay */
|
||||
@media (max-width: 768px) {
|
||||
.scan-progress-container {
|
||||
padding: var(--spacing-lg);
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
.scan-progress-stats {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.scan-stat {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0 var(--spacing-md);
|
||||
}
|
||||
|
||||
.scan-stat-value {
|
||||
font-size: var(--font-size-title);
|
||||
}
|
||||
}
|
||||
@ -202,19 +202,22 @@ class AniWorldApp {
|
||||
this.updateConnectionStatus();
|
||||
});
|
||||
|
||||
// Scan events
|
||||
this.socket.on('scan_started', () => {
|
||||
this.showStatus('Scanning series...', true);
|
||||
// Scan events - handle new detailed scan progress overlay
|
||||
this.socket.on('scan_started', (data) => {
|
||||
console.log('Scan started:', data);
|
||||
this.showScanProgressOverlay(data);
|
||||
this.updateProcessStatus('rescan', true);
|
||||
});
|
||||
|
||||
this.socket.on('scan_progress', (data) => {
|
||||
this.updateStatus(`Scanning: ${data.folder} (${data.counter})`);
|
||||
console.log('Scan progress:', data);
|
||||
this.updateScanProgressOverlay(data);
|
||||
});
|
||||
|
||||
// Handle both 'scan_completed' (legacy) and 'scan_complete' (new backend)
|
||||
const handleScanComplete = () => {
|
||||
this.hideStatus();
|
||||
const handleScanComplete = (data) => {
|
||||
console.log('Scan completed:', data);
|
||||
this.hideScanProgressOverlay(data);
|
||||
this.showToast('Scan completed successfully', 'success');
|
||||
this.updateProcessStatus('rescan', false);
|
||||
this.loadSeries();
|
||||
@ -1074,6 +1077,157 @@ class AniWorldApp {
|
||||
document.getElementById('status-panel').classList.add('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the scan progress overlay with spinner and initial state
|
||||
* @param {Object} data - Scan started event data
|
||||
*/
|
||||
showScanProgressOverlay(data) {
|
||||
// Remove existing overlay if present
|
||||
this.removeScanProgressOverlay();
|
||||
|
||||
// Create overlay element
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'scan-progress-overlay';
|
||||
overlay.className = 'scan-progress-overlay';
|
||||
overlay.innerHTML = `
|
||||
<div class="scan-progress-container">
|
||||
<div class="scan-progress-header">
|
||||
<h3>
|
||||
<span class="scan-progress-spinner"></span>
|
||||
<i class="fas fa-check-circle scan-completed-icon"></i>
|
||||
<span class="scan-title-text">Scanning Library</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="scan-progress-stats">
|
||||
<div class="scan-stat">
|
||||
<span class="scan-stat-value" id="scan-directories-count">0</span>
|
||||
<span class="scan-stat-label">Directories</span>
|
||||
</div>
|
||||
<div class="scan-stat">
|
||||
<span class="scan-stat-value" id="scan-files-count">0</span>
|
||||
<span class="scan-stat-label">Series Found</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scan-current-directory" id="scan-current-directory">
|
||||
<span class="scan-current-directory-label">Scanning:</span>
|
||||
<span id="scan-current-path">${this.escapeHtml(data?.directory || 'Initializing...')}</span>
|
||||
</div>
|
||||
<div class="scan-elapsed-time hidden" id="scan-elapsed-time">
|
||||
<i class="fas fa-clock"></i>
|
||||
<span id="scan-elapsed-value">0.0s</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Trigger animation by adding visible class after a brief delay
|
||||
requestAnimationFrame(() => {
|
||||
overlay.classList.add('visible');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the scan progress overlay with current progress
|
||||
* @param {Object} data - Scan progress event data
|
||||
*/
|
||||
updateScanProgressOverlay(data) {
|
||||
const overlay = document.getElementById('scan-progress-overlay');
|
||||
if (!overlay) return;
|
||||
|
||||
// Update directories count
|
||||
const dirCount = document.getElementById('scan-directories-count');
|
||||
if (dirCount && data.directories_scanned !== undefined) {
|
||||
dirCount.textContent = data.directories_scanned;
|
||||
}
|
||||
|
||||
// Update files/series count
|
||||
const filesCount = document.getElementById('scan-files-count');
|
||||
if (filesCount && data.files_found !== undefined) {
|
||||
filesCount.textContent = data.files_found;
|
||||
}
|
||||
|
||||
// Update current directory (truncate if too long)
|
||||
const currentPath = document.getElementById('scan-current-path');
|
||||
if (currentPath && data.current_directory) {
|
||||
const maxLength = 50;
|
||||
let displayPath = data.current_directory;
|
||||
if (displayPath.length > maxLength) {
|
||||
displayPath = '...' + displayPath.slice(-maxLength + 3);
|
||||
}
|
||||
currentPath.textContent = displayPath;
|
||||
currentPath.title = data.current_directory; // Full path on hover
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the scan progress overlay with completion summary
|
||||
* @param {Object} data - Scan completed event data
|
||||
*/
|
||||
hideScanProgressOverlay(data) {
|
||||
const overlay = document.getElementById('scan-progress-overlay');
|
||||
if (!overlay) return;
|
||||
|
||||
const container = overlay.querySelector('.scan-progress-container');
|
||||
if (container) {
|
||||
container.classList.add('completed');
|
||||
}
|
||||
|
||||
// Update title
|
||||
const titleText = overlay.querySelector('.scan-title-text');
|
||||
if (titleText) {
|
||||
titleText.textContent = 'Scan Complete';
|
||||
}
|
||||
|
||||
// Update final stats
|
||||
if (data) {
|
||||
const dirCount = document.getElementById('scan-directories-count');
|
||||
if (dirCount && data.total_directories !== undefined) {
|
||||
dirCount.textContent = data.total_directories;
|
||||
}
|
||||
|
||||
const filesCount = document.getElementById('scan-files-count');
|
||||
if (filesCount && data.total_files !== undefined) {
|
||||
filesCount.textContent = data.total_files;
|
||||
}
|
||||
|
||||
// Show elapsed time
|
||||
const elapsedTimeEl = document.getElementById('scan-elapsed-time');
|
||||
const elapsedValueEl = document.getElementById('scan-elapsed-value');
|
||||
if (elapsedTimeEl && elapsedValueEl && data.elapsed_seconds !== undefined) {
|
||||
elapsedValueEl.textContent = `${data.elapsed_seconds.toFixed(1)}s`;
|
||||
elapsedTimeEl.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Update current directory to show completion message
|
||||
const currentPath = document.getElementById('scan-current-path');
|
||||
if (currentPath) {
|
||||
currentPath.textContent = 'Scan finished successfully';
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-dismiss after 3 seconds
|
||||
setTimeout(() => {
|
||||
this.removeScanProgressOverlay();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the scan progress overlay from the DOM
|
||||
*/
|
||||
removeScanProgressOverlay() {
|
||||
const overlay = document.getElementById('scan-progress-overlay');
|
||||
if (overlay) {
|
||||
overlay.classList.remove('visible');
|
||||
// Wait for fade out animation before removing
|
||||
setTimeout(() => {
|
||||
if (overlay.parentElement) {
|
||||
overlay.remove();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
document.getElementById('loading-overlay').classList.remove('hidden');
|
||||
}
|
||||
|
||||
@ -433,6 +433,63 @@ class TestWebSocketService:
|
||||
assert call_args["data"]["code"] == error_code
|
||||
assert call_args["data"]["message"] == error_message
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_broadcast_scan_started(self, service, mock_websocket):
|
||||
"""Test broadcasting scan started event."""
|
||||
connection_id = "test-conn"
|
||||
directory = "/home/user/anime"
|
||||
|
||||
await service.connect(mock_websocket, connection_id)
|
||||
await service.broadcast_scan_started(directory)
|
||||
|
||||
assert mock_websocket.send_json.called
|
||||
call_args = mock_websocket.send_json.call_args[0][0]
|
||||
assert call_args["type"] == "scan_started"
|
||||
assert call_args["data"]["directory"] == directory
|
||||
assert "timestamp" in call_args
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_broadcast_scan_progress(self, service, mock_websocket):
|
||||
"""Test broadcasting scan progress event."""
|
||||
connection_id = "test-conn"
|
||||
directories_scanned = 25
|
||||
files_found = 150
|
||||
current_directory = "/home/user/anime/Attack on Titan"
|
||||
|
||||
await service.connect(mock_websocket, connection_id)
|
||||
await service.broadcast_scan_progress(
|
||||
directories_scanned, files_found, current_directory
|
||||
)
|
||||
|
||||
assert mock_websocket.send_json.called
|
||||
call_args = mock_websocket.send_json.call_args[0][0]
|
||||
assert call_args["type"] == "scan_progress"
|
||||
assert call_args["data"]["directories_scanned"] == directories_scanned
|
||||
assert call_args["data"]["files_found"] == files_found
|
||||
assert call_args["data"]["current_directory"] == current_directory
|
||||
assert "timestamp" in call_args
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_broadcast_scan_completed(self, service, mock_websocket):
|
||||
"""Test broadcasting scan completed event."""
|
||||
connection_id = "test-conn"
|
||||
total_directories = 100
|
||||
total_files = 500
|
||||
elapsed_seconds = 12.5
|
||||
|
||||
await service.connect(mock_websocket, connection_id)
|
||||
await service.broadcast_scan_completed(
|
||||
total_directories, total_files, elapsed_seconds
|
||||
)
|
||||
|
||||
assert mock_websocket.send_json.called
|
||||
call_args = mock_websocket.send_json.call_args[0][0]
|
||||
assert call_args["type"] == "scan_completed"
|
||||
assert call_args["data"]["total_directories"] == total_directories
|
||||
assert call_args["data"]["total_files"] == total_files
|
||||
assert call_args["data"]["elapsed_seconds"] == elapsed_seconds
|
||||
assert "timestamp" in call_args
|
||||
|
||||
|
||||
class TestGetWebSocketService:
|
||||
"""Test cases for get_websocket_service factory function."""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user