NoDataFile #1
Binary file not shown.
Binary file not shown.
@ -17,8 +17,7 @@
|
||||
"keep_days": 30
|
||||
},
|
||||
"other": {
|
||||
"master_password_hash": "$pbkdf2-sha256$29000$o1RqTSnFWKt1TknpHQOgdA$ZYtZ.NZkQbLYYhbQNJXUl7NOotcBza58uEIrhnP9M9Q",
|
||||
"anime_directory": "/mnt/server/serien/Serien/"
|
||||
"master_password_hash": "$pbkdf2-sha256$29000$bY3xHiPkPCckJMT4H8PY2w$s7wlQnFrLpXdGE4GhX5hgZGSHka4SsuAchcFN5qBx3k"
|
||||
},
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@ -17,8 +17,7 @@
|
||||
"keep_days": 30
|
||||
},
|
||||
"other": {
|
||||
"master_password_hash": "$pbkdf2-sha256$29000$RYgRIoQwBuC8N.bcO0eoNQ$6Cdc9sZvqy8li/43B0NcXYlysYrj/lIqy2E7gBtN4dk",
|
||||
"anime_directory": "/mnt/server/serien/Serien/"
|
||||
"master_password_hash": "$pbkdf2-sha256$29000$CMG4t9a6t9Y6J2TMOYfQ2g$bUIhqeewMMSj2Heh07vIhAGjvzDVijHb62aes4JuJhw"
|
||||
},
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@ -446,12 +446,17 @@ class SeriesApp:
|
||||
|
||||
try:
|
||||
# Get total items to scan
|
||||
logger.info("Getting total items to scan...")
|
||||
total_to_scan = await asyncio.to_thread(
|
||||
self.serie_scanner.get_total_to_scan
|
||||
)
|
||||
logger.info("Total folders to scan: %d", total_to_scan)
|
||||
|
||||
# Fire scan started event
|
||||
logger.info(
|
||||
"Firing scan_status 'started' event, handler=%s",
|
||||
self._events.scan_status
|
||||
)
|
||||
self._events.scan_status(
|
||||
ScanStatusEventArgs(
|
||||
current=0,
|
||||
@ -502,6 +507,10 @@ class SeriesApp:
|
||||
logger.info("Directory rescan completed successfully")
|
||||
|
||||
# Fire scan completed event
|
||||
logger.info(
|
||||
"Firing scan_status 'completed' event, handler=%s",
|
||||
self._events.scan_status
|
||||
)
|
||||
self._events.scan_status(
|
||||
ScanStatusEventArgs(
|
||||
current=total_to_scan,
|
||||
|
||||
@ -53,12 +53,17 @@ class AnimeService:
|
||||
self._scan_start_time: Optional[float] = None
|
||||
self._scan_directories_count: int = 0
|
||||
self._scan_files_count: int = 0
|
||||
self._scan_total_items: int = 0
|
||||
# Subscribe to SeriesApp events
|
||||
# Note: Events library uses assignment (=), not += operator
|
||||
try:
|
||||
self._app.download_status = self._on_download_status
|
||||
self._app.scan_status = self._on_scan_status
|
||||
logger.debug("Successfully subscribed to SeriesApp events")
|
||||
logger.info(
|
||||
"Subscribed to SeriesApp events",
|
||||
scan_status_handler=str(self._app.scan_status),
|
||||
series_app_id=id(self._app),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("Failed to subscribe to SeriesApp events")
|
||||
raise AnimeServiceError("Initialization failed") from e
|
||||
@ -173,20 +178,39 @@ class AnimeService:
|
||||
try:
|
||||
scan_id = "library_scan"
|
||||
|
||||
logger.info(
|
||||
"Scan status event received",
|
||||
status=args.status,
|
||||
current=args.current,
|
||||
total=args.total,
|
||||
folder=args.folder,
|
||||
)
|
||||
|
||||
# Get event loop - try running loop first, then stored loop
|
||||
loop = None
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
logger.debug("Using running event loop for scan status")
|
||||
except RuntimeError:
|
||||
# No running loop in this thread - use stored loop
|
||||
loop = self._event_loop
|
||||
logger.debug(
|
||||
"Using stored event loop for scan status",
|
||||
has_loop=loop is not None
|
||||
)
|
||||
|
||||
if not loop:
|
||||
logger.debug(
|
||||
logger.warning(
|
||||
"No event loop available for scan status event",
|
||||
status=args.status
|
||||
)
|
||||
return
|
||||
|
||||
logger.info(
|
||||
"Processing scan status event",
|
||||
status=args.status,
|
||||
loop_id=id(loop),
|
||||
)
|
||||
|
||||
# Map SeriesApp scan events to progress service
|
||||
if args.status == "started":
|
||||
@ -194,6 +218,7 @@ class AnimeService:
|
||||
self._scan_start_time = time.time()
|
||||
self._scan_directories_count = 0
|
||||
self._scan_files_count = 0
|
||||
self._scan_total_items = args.total
|
||||
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._progress_service.start_progress(
|
||||
@ -204,9 +229,9 @@ class AnimeService:
|
||||
),
|
||||
loop
|
||||
)
|
||||
# Broadcast scan started via WebSocket
|
||||
# Broadcast scan started via WebSocket with total items
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._broadcast_scan_started_safe(),
|
||||
self._broadcast_scan_started_safe(total_items=args.total),
|
||||
loop
|
||||
)
|
||||
elif args.status == "progress":
|
||||
@ -224,12 +249,13 @@ class AnimeService:
|
||||
),
|
||||
loop
|
||||
)
|
||||
# Broadcast scan progress via WebSocket (throttled - every update)
|
||||
# Broadcast scan progress via WebSocket
|
||||
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 "",
|
||||
total_items=args.total,
|
||||
),
|
||||
loop
|
||||
)
|
||||
@ -274,16 +300,26 @@ 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:
|
||||
async def _broadcast_scan_started_safe(self, total_items: int = 0) -> None:
|
||||
"""Safely broadcast scan started event via WebSocket.
|
||||
|
||||
Wraps the WebSocket broadcast in try/except to ensure scan
|
||||
continues even if WebSocket fails.
|
||||
|
||||
Args:
|
||||
total_items: Total number of items to scan
|
||||
"""
|
||||
try:
|
||||
await self._websocket_service.broadcast_scan_started(
|
||||
directory=self._directory
|
||||
logger.info(
|
||||
"Broadcasting scan_started via WebSocket",
|
||||
directory=self._directory,
|
||||
total_items=total_items,
|
||||
)
|
||||
await self._websocket_service.broadcast_scan_started(
|
||||
directory=self._directory,
|
||||
total_items=total_items,
|
||||
)
|
||||
logger.info("scan_started broadcast sent successfully")
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Failed to broadcast scan_started via WebSocket",
|
||||
@ -295,6 +331,7 @@ class AnimeService:
|
||||
directories_scanned: int,
|
||||
files_found: int,
|
||||
current_directory: str,
|
||||
total_items: int = 0,
|
||||
) -> None:
|
||||
"""Safely broadcast scan progress event via WebSocket.
|
||||
|
||||
@ -305,12 +342,14 @@ class AnimeService:
|
||||
directories_scanned: Number of directories scanned so far
|
||||
files_found: Number of files found so far
|
||||
current_directory: Current directory being scanned
|
||||
total_items: Total number of items to scan
|
||||
"""
|
||||
try:
|
||||
await self._websocket_service.broadcast_scan_progress(
|
||||
directories_scanned=directories_scanned,
|
||||
files_found=files_found,
|
||||
current_directory=current_directory,
|
||||
total_items=total_items,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
@ -418,6 +457,12 @@ class AnimeService:
|
||||
try:
|
||||
# Store event loop for event handlers
|
||||
self._event_loop = asyncio.get_running_loop()
|
||||
logger.info(
|
||||
"Rescan started, event loop stored",
|
||||
loop_id=id(self._event_loop),
|
||||
series_app_id=id(self._app),
|
||||
scan_handler=str(self._app.scan_status),
|
||||
)
|
||||
|
||||
# SeriesApp.rescan returns scanned series list
|
||||
scanned_series = await self._app.rescan()
|
||||
|
||||
@ -498,27 +498,36 @@ class WebSocketService:
|
||||
}
|
||||
await self._manager.send_personal_message(message, connection_id)
|
||||
|
||||
async def broadcast_scan_started(self, directory: str) -> None:
|
||||
async def broadcast_scan_started(
|
||||
self, directory: str, total_items: int = 0
|
||||
) -> None:
|
||||
"""Broadcast that a library scan has started.
|
||||
|
||||
Args:
|
||||
directory: The root directory path being scanned
|
||||
total_items: Total number of items to scan (for progress display)
|
||||
"""
|
||||
message = {
|
||||
"type": "scan_started",
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"data": {
|
||||
"directory": directory,
|
||||
"total_items": total_items,
|
||||
},
|
||||
}
|
||||
await self._manager.broadcast(message)
|
||||
logger.info("Broadcast scan_started", directory=directory)
|
||||
logger.info(
|
||||
"Broadcast scan_started",
|
||||
directory=directory,
|
||||
total_items=total_items,
|
||||
)
|
||||
|
||||
async def broadcast_scan_progress(
|
||||
self,
|
||||
directories_scanned: int,
|
||||
files_found: int,
|
||||
current_directory: str,
|
||||
total_items: int = 0,
|
||||
) -> None:
|
||||
"""Broadcast scan progress update to all clients.
|
||||
|
||||
@ -526,6 +535,7 @@ class WebSocketService:
|
||||
directories_scanned: Number of directories scanned so far
|
||||
files_found: Number of MP4 files found so far
|
||||
current_directory: Current directory being scanned
|
||||
total_items: Total number of items to scan (for progress display)
|
||||
"""
|
||||
message = {
|
||||
"type": "scan_progress",
|
||||
@ -534,6 +544,7 @@ class WebSocketService:
|
||||
"directories_scanned": directories_scanned,
|
||||
"files_found": files_found,
|
||||
"current_directory": current_directory,
|
||||
"total_items": total_items,
|
||||
},
|
||||
}
|
||||
await self._manager.broadcast(message)
|
||||
|
||||
@ -1978,6 +1978,47 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* Progress bar for scan */
|
||||
.scan-progress-bar-container {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background-color: var(--color-bg-tertiary);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.scan-progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--color-accent), var(--color-accent-hover, var(--color-accent)));
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.scan-progress-container.completed .scan-progress-bar {
|
||||
background: linear-gradient(90deg, var(--color-success), var(--color-success));
|
||||
}
|
||||
|
||||
.scan-progress-text {
|
||||
font-size: var(--font-size-body);
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.scan-progress-text #scan-current-count {
|
||||
font-weight: 600;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.scan-progress-text #scan-total-count {
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.scan-progress-container.completed .scan-progress-text #scan-current-count {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.scan-progress-stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
|
||||
@ -1085,10 +1085,16 @@ class AniWorldApp {
|
||||
// Remove existing overlay if present
|
||||
this.removeScanProgressOverlay();
|
||||
|
||||
// Store total items for progress calculation
|
||||
this.scanTotalItems = data?.total_items || 0;
|
||||
|
||||
// Create overlay element
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'scan-progress-overlay';
|
||||
overlay.className = 'scan-progress-overlay';
|
||||
|
||||
const totalDisplay = this.scanTotalItems > 0 ? this.scanTotalItems : '...';
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div class="scan-progress-container">
|
||||
<div class="scan-progress-header">
|
||||
@ -1098,10 +1104,16 @@ class AniWorldApp {
|
||||
<span class="scan-title-text">Scanning Library</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="scan-progress-bar-container">
|
||||
<div class="scan-progress-bar" id="scan-progress-bar" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="scan-progress-text" id="scan-progress-text">
|
||||
<span id="scan-current-count">0</span> / <span id="scan-total-count">${totalDisplay}</span> directories
|
||||
</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>
|
||||
<span class="scan-stat-label">Scanned</span>
|
||||
</div>
|
||||
<div class="scan-stat">
|
||||
<span class="scan-stat-value" id="scan-files-count">0</span>
|
||||
@ -1109,7 +1121,7 @@ class AniWorldApp {
|
||||
</div>
|
||||
</div>
|
||||
<div class="scan-current-directory" id="scan-current-directory">
|
||||
<span class="scan-current-directory-label">Scanning:</span>
|
||||
<span class="scan-current-directory-label">Current:</span>
|
||||
<span id="scan-current-path">${this.escapeHtml(data?.directory || 'Initializing...')}</span>
|
||||
</div>
|
||||
<div class="scan-elapsed-time hidden" id="scan-elapsed-time">
|
||||
@ -1135,6 +1147,28 @@ class AniWorldApp {
|
||||
const overlay = document.getElementById('scan-progress-overlay');
|
||||
if (!overlay) return;
|
||||
|
||||
// Update total items if provided (in case it wasn't available at start)
|
||||
if (data.total_items && data.total_items > 0) {
|
||||
this.scanTotalItems = data.total_items;
|
||||
const totalCount = document.getElementById('scan-total-count');
|
||||
if (totalCount) {
|
||||
totalCount.textContent = this.scanTotalItems;
|
||||
}
|
||||
}
|
||||
|
||||
// Update progress bar
|
||||
const progressBar = document.getElementById('scan-progress-bar');
|
||||
if (progressBar && this.scanTotalItems > 0 && data.directories_scanned !== undefined) {
|
||||
const percentage = Math.min(100, (data.directories_scanned / this.scanTotalItems) * 100);
|
||||
progressBar.style.width = `${percentage}%`;
|
||||
}
|
||||
|
||||
// Update current/total count display
|
||||
const currentCount = document.getElementById('scan-current-count');
|
||||
if (currentCount && data.directories_scanned !== undefined) {
|
||||
currentCount.textContent = data.directories_scanned;
|
||||
}
|
||||
|
||||
// Update directories count
|
||||
const dirCount = document.getElementById('scan-directories-count');
|
||||
if (dirCount && data.directories_scanned !== undefined) {
|
||||
@ -1179,6 +1213,12 @@ class AniWorldApp {
|
||||
titleText.textContent = 'Scan Complete';
|
||||
}
|
||||
|
||||
// Complete the progress bar
|
||||
const progressBar = document.getElementById('scan-progress-bar');
|
||||
if (progressBar) {
|
||||
progressBar.style.width = '100%';
|
||||
}
|
||||
|
||||
// Update final stats
|
||||
if (data) {
|
||||
const dirCount = document.getElementById('scan-directories-count');
|
||||
@ -1191,6 +1231,16 @@ class AniWorldApp {
|
||||
filesCount.textContent = data.total_files;
|
||||
}
|
||||
|
||||
// Update progress text to show final count
|
||||
const currentCount = document.getElementById('scan-current-count');
|
||||
const totalCount = document.getElementById('scan-total-count');
|
||||
if (currentCount && data.total_directories !== undefined) {
|
||||
currentCount.textContent = data.total_directories;
|
||||
}
|
||||
if (totalCount && data.total_directories !== undefined) {
|
||||
totalCount.textContent = data.total_directories;
|
||||
}
|
||||
|
||||
// Show elapsed time
|
||||
const elapsedTimeEl = document.getElementById('scan-elapsed-time');
|
||||
const elapsedValueEl = document.getElementById('scan-elapsed-value');
|
||||
|
||||
@ -438,6 +438,23 @@ class TestWebSocketService:
|
||||
"""Test broadcasting scan started event."""
|
||||
connection_id = "test-conn"
|
||||
directory = "/home/user/anime"
|
||||
total_items = 42
|
||||
|
||||
await service.connect(mock_websocket, connection_id)
|
||||
await service.broadcast_scan_started(directory, total_items)
|
||||
|
||||
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 call_args["data"]["total_items"] == total_items
|
||||
assert "timestamp" in call_args
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_broadcast_scan_started_default_total(self, service, mock_websocket):
|
||||
"""Test broadcasting scan started event with default total_items."""
|
||||
connection_id = "test-conn"
|
||||
directory = "/home/user/anime"
|
||||
|
||||
await service.connect(mock_websocket, connection_id)
|
||||
await service.broadcast_scan_started(directory)
|
||||
@ -446,6 +463,7 @@ class TestWebSocketService:
|
||||
call_args = mock_websocket.send_json.call_args[0][0]
|
||||
assert call_args["type"] == "scan_started"
|
||||
assert call_args["data"]["directory"] == directory
|
||||
assert call_args["data"]["total_items"] == 0
|
||||
assert "timestamp" in call_args
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@ -455,6 +473,29 @@ class TestWebSocketService:
|
||||
directories_scanned = 25
|
||||
files_found = 150
|
||||
current_directory = "/home/user/anime/Attack on Titan"
|
||||
total_items = 100
|
||||
|
||||
await service.connect(mock_websocket, connection_id)
|
||||
await service.broadcast_scan_progress(
|
||||
directories_scanned, files_found, current_directory, total_items
|
||||
)
|
||||
|
||||
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 call_args["data"]["total_items"] == total_items
|
||||
assert "timestamp" in call_args
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_broadcast_scan_progress_default_total(self, service, mock_websocket):
|
||||
"""Test broadcasting scan progress event with default total_items."""
|
||||
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(
|
||||
@ -467,6 +508,7 @@ class TestWebSocketService:
|
||||
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 call_args["data"]["total_items"] == 0
|
||||
assert "timestamp" in call_args
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user