Skip to content

Commit 05d1944

Browse files
committed
Add pending scans tracking and display
- Added PENDING_SCANS dictionary to track scan requests - Created add_pending_scan() and check_scan_status() helper functions - Updated scan endpoints to track pending scans with unique IDs - Added /api/plex/pending-scans endpoint to get pending scan list - Added UI section to display pending scans with status indicators - Auto-refreshes pending scans every 5 seconds - Automatically removes completed scans older than 1 hour - Shows scan type, name, status (pending/completed/failed), and timestamps
1 parent c73de22 commit 05d1944

3 files changed

Lines changed: 269 additions & 6 deletions

File tree

media_watcher_service.py

Lines changed: 142 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44
import asyncio
55
import logging
6+
import uuid
67
from collections import deque, defaultdict
78
from datetime import datetime
89
from flask import Flask, request, jsonify, send_from_directory
@@ -34,11 +35,82 @@
3435
OVERSEERR_USERS_DATA: Dict[str, dict] = {}
3536
NOTIFICATION_HISTORY: Deque[Dict[str, Any]] = deque(
3637
maxlen=100) # Store last 100 notifications
38+
PENDING_SCANS: Dict[str, Dict[str, Any]] = {} # Track pending scans by scan_id
3739

3840
DEBOUNCE_SECONDS = 60
3941

4042
app = Flask(__name__)
4143

44+
45+
def add_pending_scan(scan_type: str, scan_id: str, name: str, library_key: str = None, item_key: str = None):
46+
"""Add a scan to the pending scans list."""
47+
PENDING_SCANS[scan_id] = {
48+
"scan_id": scan_id,
49+
"type": scan_type, # 'library', 'item', 'all_libraries'
50+
"name": name,
51+
"library_key": library_key,
52+
"item_key": item_key,
53+
"status": "pending",
54+
"timestamp": datetime.now().isoformat(),
55+
"checked_at": datetime.now().isoformat()
56+
}
57+
logger.info(f"Added pending scan: {scan_id} ({scan_type}) - {name}")
58+
59+
60+
def check_scan_status(scan_id: str) -> str:
61+
"""Check if a scan is still pending or completed."""
62+
if scan_id not in PENDING_SCANS:
63+
return "unknown"
64+
65+
scan_info = PENDING_SCANS[scan_id]
66+
scan_type = scan_info.get("type")
67+
library_key = scan_info.get("library_key")
68+
69+
if scan_type == "library" and library_key:
70+
# Check if library is still scanning
71+
from plex_utils import is_plex_scanning
72+
try:
73+
section_id = int(library_key)
74+
is_scanning = is_plex_scanning(section_id)
75+
if not is_scanning:
76+
# Scan likely completed
77+
scan_info["status"] = "completed"
78+
scan_info["completed_at"] = datetime.now().isoformat()
79+
return "completed"
80+
return "pending"
81+
except Exception as e:
82+
logger.warning(f"Error checking scan status for {scan_id}: {e}")
83+
# Assume completed after a timeout period
84+
scan_timestamp = datetime.fromisoformat(scan_info["timestamp"])
85+
time_since_scan = (datetime.now() - scan_timestamp).total_seconds()
86+
if time_since_scan > 300: # 5 minutes
87+
scan_info["status"] = "completed"
88+
scan_info["completed_at"] = datetime.now().isoformat()
89+
return "completed"
90+
return "pending"
91+
elif scan_type == "all_libraries":
92+
# For all libraries scan, mark as completed after a reasonable time
93+
scan_timestamp = datetime.fromisoformat(scan_info["timestamp"])
94+
time_since_scan = (datetime.now() - scan_timestamp).total_seconds()
95+
# Assume completed after 10 minutes for all libraries
96+
if time_since_scan > 600:
97+
scan_info["status"] = "completed"
98+
scan_info["completed_at"] = datetime.now().isoformat()
99+
return "completed"
100+
return "pending"
101+
else:
102+
# For item scans, check the library status
103+
if library_key:
104+
return check_scan_status(f"library_{library_key}")
105+
# Default: assume completed after 5 minutes
106+
scan_timestamp = datetime.fromisoformat(scan_info["timestamp"])
107+
time_since_scan = (datetime.now() - scan_timestamp).total_seconds()
108+
if time_since_scan > 300:
109+
scan_info["status"] = "completed"
110+
scan_info["completed_at"] = datetime.now().isoformat()
111+
return "completed"
112+
return "pending"
113+
42114
# --- Core Notification Logic ---
43115

44116

@@ -822,6 +894,9 @@ def api_plex_scan():
822894
def api_plex_scan_all():
823895
"""Sequentially scans all Plex libraries, waiting for each to complete."""
824896
try:
897+
scan_id = f"all_libraries_{uuid.uuid4().hex[:8]}"
898+
add_pending_scan("all_libraries", scan_id, "All Libraries")
899+
825900
bot_instance = app.config.get('discord_bot')
826901
if not bot_instance:
827902
return jsonify({"success": False, "message": "Bot instance not available"}), 500
@@ -837,6 +912,7 @@ def api_plex_scan_all():
837912
else:
838913
result = asyncio.run(scan_all_libraries_sequential_async())
839914

915+
result["scan_id"] = scan_id
840916
return jsonify(result)
841917

842918
except Exception as e:
@@ -895,20 +971,39 @@ def api_plex_item_scan():
895971
try:
896972
data = request.json or {}
897973
item_key = data.get('item_key')
898-
974+
item_name = data.get('item_name', 'Unknown Item')
975+
899976
if not item_key:
900977
return jsonify({
901978
"success": False,
902979
"message": "Missing item_key in request body"
903980
}), 400
904-
981+
905982
logger.info(f"Received scan request for item key: {item_key}")
906-
983+
907984
bot_instance = app.config.get('discord_bot')
908985
if not bot_instance:
909986
logger.error("Bot instance not available for item scan")
910987
return jsonify({"success": False, "message": "Bot instance not available"}), 500
911988

989+
# Get library key from item if possible
990+
library_key = None
991+
try:
992+
from plex_utils import get_plex_client
993+
plex = get_plex_client()
994+
if plex:
995+
item = plex.fetchItem(item_key)
996+
if item:
997+
section = item.section()
998+
if section:
999+
library_key = str(section.key)
1000+
except Exception as e:
1001+
logger.warning(f"Could not determine library for item: {e}")
1002+
1003+
# Create scan ID and add to pending scans
1004+
scan_id = f"item_{uuid.uuid4().hex[:8]}"
1005+
add_pending_scan("item", scan_id, item_name, library_key=library_key, item_key=item_key)
1006+
9121007
logger.info(f"Starting async scan for item: {item_key}")
9131008
# Run scan in async context
9141009
loop = bot_instance.loop
@@ -933,10 +1028,14 @@ def api_plex_item_scan():
9331028
if result:
9341029
return jsonify({
9351030
"success": True,
936-
"message": "Successfully triggered Plex scan for item"
1031+
"message": "Successfully triggered Plex scan for item",
1032+
"scan_id": scan_id
9371033
})
9381034
else:
9391035
logger.warning(f"Scan returned False for item: {item_key}")
1036+
# Remove from pending if it failed
1037+
if scan_id in PENDING_SCANS:
1038+
PENDING_SCANS[scan_id]["status"] = "failed"
9401039
return jsonify({
9411040
"success": False,
9421041
"message": "Failed to trigger Plex scan for item. Check server logs for details."
@@ -950,6 +1049,45 @@ def api_plex_item_scan():
9501049
"message": f"Error: {str(e)}"
9511050
}), 500
9521051

1052+
1053+
@app.route('/api/plex/pending-scans', methods=['GET'])
1054+
def api_pending_scans():
1055+
"""Gets the list of pending scans."""
1056+
try:
1057+
# Update status for all pending scans
1058+
for scan_id in list(PENDING_SCANS.keys()):
1059+
scan_info = PENDING_SCANS[scan_id]
1060+
if scan_info.get("status") == "pending":
1061+
check_scan_status(scan_id) # Updates status internally
1062+
scan_info["checked_at"] = datetime.now().isoformat()
1063+
1064+
# Filter out completed scans older than 1 hour
1065+
now = datetime.now()
1066+
active_scans = []
1067+
for scan_id, scan_info in PENDING_SCANS.items():
1068+
if scan_info.get("status") == "completed":
1069+
completed_at = scan_info.get("completed_at")
1070+
if completed_at:
1071+
completed_time = datetime.fromisoformat(completed_at)
1072+
if (now - completed_time).total_seconds() > 3600: # 1 hour
1073+
continue
1074+
active_scans.append(scan_info)
1075+
1076+
# Sort by timestamp, newest first
1077+
active_scans.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
1078+
1079+
return jsonify({
1080+
"success": True,
1081+
"pending_scans": active_scans
1082+
})
1083+
except Exception as e:
1084+
logger.error(f"Error getting pending scans: {e}", exc_info=True)
1085+
return jsonify({
1086+
"success": False,
1087+
"message": f"Error: {str(e)}",
1088+
"pending_scans": []
1089+
}), 500
1090+
9531091
# --- Service Setup ---
9541092

9551093

webui/src/App.css

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,3 +1076,80 @@ body {
10761076
font-size: 0.75rem;
10771077
color: var(--text-secondary);
10781078
}
1079+
1080+
/* Pending Scans Section */
1081+
.pending-scans-section {
1082+
background: var(--bg-secondary);
1083+
border-radius: 1rem;
1084+
padding: 1.5rem;
1085+
border: 1px solid var(--border);
1086+
margin-bottom: 2rem;
1087+
}
1088+
1089+
.pending-scans-section h2 {
1090+
margin-bottom: 1rem;
1091+
font-size: 1.5rem;
1092+
}
1093+
1094+
.pending-scans-list {
1095+
display: flex;
1096+
flex-direction: column;
1097+
gap: 0.75rem;
1098+
}
1099+
1100+
.pending-scan-card {
1101+
background: var(--bg-tertiary);
1102+
border: 1px solid var(--border);
1103+
border-radius: 0.5rem;
1104+
padding: 1rem;
1105+
}
1106+
1107+
.pending-scan-header {
1108+
display: flex;
1109+
justify-content: space-between;
1110+
align-items: center;
1111+
margin-bottom: 0.5rem;
1112+
}
1113+
1114+
.pending-scan-info {
1115+
display: flex;
1116+
align-items: center;
1117+
gap: 0.75rem;
1118+
}
1119+
1120+
.pending-scan-type {
1121+
font-size: 0.75rem;
1122+
text-transform: uppercase;
1123+
color: var(--text-secondary);
1124+
background: var(--bg-primary);
1125+
padding: 0.25rem 0.5rem;
1126+
border-radius: 0.25rem;
1127+
font-weight: 600;
1128+
}
1129+
1130+
.pending-scan-name {
1131+
font-weight: 500;
1132+
color: var(--text-primary);
1133+
}
1134+
1135+
.pending-scan-status {
1136+
font-size: 0.875rem;
1137+
font-weight: 500;
1138+
}
1139+
1140+
.pending-scan-status.pending {
1141+
color: #f59e0b;
1142+
}
1143+
1144+
.pending-scan-status.completed {
1145+
color: var(--success);
1146+
}
1147+
1148+
.pending-scan-status.failed {
1149+
color: var(--error);
1150+
}
1151+
1152+
.pending-scan-time {
1153+
font-size: 0.875rem;
1154+
color: var(--text-secondary);
1155+
}

webui/src/App.jsx

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,16 @@ function App() {
4141
const [loadingItems, setLoadingItems] = useState(false);
4242
const [scanningAll, setScanningAll] = useState(false);
4343
const [scanAllProgress, setScanAllProgress] = useState(null);
44+
const [pendingScans, setPendingScans] = useState([]);
4445

4546
useEffect(() => {
4647
fetchStatus();
4748
fetchNotifications();
49+
fetchPendingScans();
4850
const interval = setInterval(() => {
4951
fetchStatus();
5052
fetchNotifications();
53+
fetchPendingScans();
5154
}, 5000); // Refresh every 5 seconds
5255
return () => clearInterval(interval);
5356
}, []);
@@ -115,14 +118,29 @@ function App() {
115118
fetchLibraryItems(library.key);
116119
};
117120

121+
const fetchPendingScans = async () => {
122+
try {
123+
const response = await fetch(`${API_BASE}/plex/pending-scans`);
124+
const data = await response.json();
125+
if (data.success) {
126+
setPendingScans(data.pending_scans || []);
127+
}
128+
} catch (error) {
129+
console.error('Failed to fetch pending scans:', error);
130+
}
131+
};
132+
118133
const handleItemScan = async (item) => {
119134
try {
120135
console.log(`Scanning item: ${item.title} (key: ${item.key})`);
121-
// Send item_key in request body to avoid URL encoding issues
136+
// Send item_key and item_name in request body
122137
const response = await fetch(`${API_BASE}/plex/item/scan`, {
123138
method: 'POST',
124139
headers: { 'Content-Type': 'application/json' },
125-
body: JSON.stringify({ item_key: item.key }),
140+
body: JSON.stringify({
141+
item_key: item.key,
142+
item_name: item.title
143+
}),
126144
});
127145

128146
if (!response.ok) {
@@ -141,6 +159,8 @@ function App() {
141159
const data = await response.json();
142160
console.log('Scan response:', data);
143161
if (data.success) {
162+
// Refresh pending scans
163+
fetchPendingScans();
144164
alert(`Successfully triggered scan for ${item.title}!`);
145165
setShowBrowseModal(false);
146166
setSelectedLibrary(null);
@@ -237,6 +257,7 @@ function App() {
237257
});
238258
const data = await response.json();
239259
setScanAllProgress(data);
260+
fetchPendingScans(); // Refresh pending scans
240261
if (data.success) {
241262
alert(`Successfully scanned ${data.scanned} of ${data.total} libraries!`);
242263
} else {
@@ -282,6 +303,33 @@ function App() {
282303
)}
283304
</section>
284305

306+
{pendingScans.length > 0 && (
307+
<section className="pending-scans-section">
308+
<h2>Pending Scans</h2>
309+
<div className="pending-scans-list">
310+
{pendingScans.map((scan) => (
311+
<div key={scan.scan_id} className="pending-scan-card">
312+
<div className="pending-scan-header">
313+
<div className="pending-scan-info">
314+
<span className="pending-scan-type">{scan.type}</span>
315+
<span className="pending-scan-name">{scan.name}</span>
316+
</div>
317+
<span className={`pending-scan-status ${scan.status}`}>
318+
{scan.status === 'pending' ? '⏳ Pending' : scan.status === 'completed' ? '✓ Completed' : '✗ Failed'}
319+
</span>
320+
</div>
321+
<div className="pending-scan-time">
322+
Started: {new Date(scan.timestamp).toLocaleString()}
323+
{scan.completed_at && (
324+
<span> • Completed: {new Date(scan.completed_at).toLocaleString()}</span>
325+
)}
326+
</div>
327+
</div>
328+
))}
329+
</div>
330+
</section>
331+
)}
332+
285333
<section className="status-section">
286334
<h2>System Status</h2>
287335
<div className="status-grid">

0 commit comments

Comments
 (0)