Skip to content

Commit 0cdafe6

Browse files
committed
Use Plex Activities API for accurate scan tracking
- Added get_plex_activities() function using /activities endpoint - Updated is_plex_scanning() to check Plex activities API first - Improved check_scan_status() to use activities for accurate status - Activities API provides real-time scan status from Plex server - Falls back to section refreshing attribute if activities unavailable - References: https://developer.plex.tv/pms/
1 parent 51e979c commit 0cdafe6

2 files changed

Lines changed: 159 additions & 38 deletions

File tree

media_watcher_service.py

Lines changed: 66 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -58,29 +58,55 @@ def add_pending_scan(scan_type: str, scan_id: str, name: str, library_key: str =
5858

5959

6060
def check_scan_status(scan_id: str) -> str:
61-
"""Check if a scan is still pending or completed."""
61+
"""Check if a scan is still pending or completed using Plex Activities API."""
6262
if scan_id not in PENDING_SCANS:
6363
return "unknown"
64-
64+
6565
scan_info = PENDING_SCANS[scan_id]
6666
scan_type = scan_info.get("type")
6767
library_key = scan_info.get("library_key")
68+
69+
# Get current activities from Plex
70+
from plex_utils import get_plex_activities, is_plex_scanning
71+
activities = get_plex_activities()
6872

73+
# Check if there are any scanning activities
74+
has_scanning_activity = len(activities) > 0
75+
6976
if scan_type == "library" and library_key:
70-
# Check if library is still scanning
71-
from plex_utils import is_plex_scanning
77+
# Check if this specific library is scanning
7278
try:
7379
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+
# Check activities for this specific section
81+
section_scanning = False
82+
for activity in activities:
83+
# Try to extract section ID from activity context
84+
# Activities might have librarySectionID in context
85+
context = activity.get('context', {})
86+
if isinstance(context, dict):
87+
act_section_id = context.get('librarySectionID') or context.get('sectionID')
88+
if act_section_id and str(act_section_id) == str(section_id):
89+
section_scanning = True
90+
logger.info(f"Found scanning activity for section {section_id}")
91+
break
92+
93+
# Also check using the section's refreshing attribute
94+
if not section_scanning:
95+
section_scanning = is_plex_scanning(section_id)
96+
97+
if not section_scanning:
98+
# No scanning activity found, check timeout
99+
scan_timestamp = datetime.fromisoformat(scan_info["timestamp"])
100+
time_since_scan = (datetime.now() - scan_timestamp).total_seconds()
101+
# If no activity and it's been more than 2 minutes, assume completed
102+
if time_since_scan > 120:
103+
scan_info["status"] = "completed"
104+
scan_info["completed_at"] = datetime.now().isoformat()
105+
return "completed"
80106
return "pending"
81107
except Exception as e:
82108
logger.warning(f"Error checking scan status for {scan_id}: {e}")
83-
# Assume completed after a timeout period
109+
# Fallback to timeout
84110
scan_timestamp = datetime.fromisoformat(scan_info["timestamp"])
85111
time_since_scan = (datetime.now() - scan_timestamp).total_seconds()
86112
if time_since_scan > 300: # 5 minutes
@@ -89,19 +115,33 @@ def check_scan_status(scan_id: str) -> str:
89115
return "completed"
90116
return "pending"
91117
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"
118+
# For all libraries scan, check if any scanning activities exist
119+
if not has_scanning_activity:
120+
# No scanning activities, check timeout
121+
scan_timestamp = datetime.fromisoformat(scan_info["timestamp"])
122+
time_since_scan = (datetime.now() - scan_timestamp).total_seconds()
123+
# If no activity and it's been more than 5 minutes, assume completed
124+
if time_since_scan > 300:
125+
scan_info["status"] = "completed"
126+
scan_info["completed_at"] = datetime.now().isoformat()
127+
return "completed"
100128
return "pending"
101129
else:
102130
# For item scans, check the library status
103131
if library_key:
104-
return check_scan_status(f"library_{library_key}")
132+
try:
133+
section_id = int(library_key)
134+
is_scanning = is_plex_scanning(section_id)
135+
if not is_scanning:
136+
scan_timestamp = datetime.fromisoformat(scan_info["timestamp"])
137+
time_since_scan = (datetime.now() - scan_timestamp).total_seconds()
138+
if time_since_scan > 120: # 2 minutes
139+
scan_info["status"] = "completed"
140+
scan_info["completed_at"] = datetime.now().isoformat()
141+
return "completed"
142+
return "pending"
143+
except:
144+
pass
105145
# Default: assume completed after 5 minutes
106146
scan_timestamp = datetime.fromisoformat(scan_info["timestamp"])
107147
time_since_scan = (datetime.now() - scan_timestamp).total_seconds()
@@ -896,7 +936,7 @@ def api_plex_scan_all():
896936
try:
897937
scan_id = f"all_libraries_{uuid.uuid4().hex[:8]}"
898938
add_pending_scan("all_libraries", scan_id, "All Libraries")
899-
939+
900940
bot_instance = app.config.get('discord_bot')
901941
if not bot_instance:
902942
return jsonify({"success": False, "message": "Bot instance not available"}), 500
@@ -1002,7 +1042,8 @@ def api_plex_item_scan():
10021042

10031043
# Create scan ID and add to pending scans
10041044
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)
1045+
add_pending_scan("item", scan_id, item_name,
1046+
library_key=library_key, item_key=item_key)
10061047

10071048
logger.info(f"Starting async scan for item: {item_key}")
10081049
# Run scan in async context
@@ -1060,7 +1101,7 @@ def api_pending_scans():
10601101
if scan_info.get("status") == "pending":
10611102
check_scan_status(scan_id) # Updates status internally
10621103
scan_info["checked_at"] = datetime.now().isoformat()
1063-
1104+
10641105
# Filter out completed scans older than 1 hour
10651106
now = datetime.now()
10661107
active_scans = []
@@ -1072,10 +1113,10 @@ def api_pending_scans():
10721113
if (now - completed_time).total_seconds() > 3600: # 1 hour
10731114
continue
10741115
active_scans.append(scan_info)
1075-
1116+
10761117
# Sort by timestamp, newest first
10771118
active_scans.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
1078-
1119+
10791120
return jsonify({
10801121
"success": True,
10811122
"pending_scans": active_scans

plex_utils.py

Lines changed: 93 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,75 @@ async def scan_plex_library_async(library_name: Optional[str] = None, media_path
157157
return await asyncio.to_thread(scan_plex_library, library_name, media_path)
158158

159159

160+
def get_plex_activities() -> list:
161+
"""
162+
Gets current activities from Plex using the /activities endpoint.
163+
Reference: https://developer.plex.tv/pms/
164+
165+
Returns:
166+
List of activity dictionaries.
167+
"""
168+
try:
169+
plex_url = os.getenv("PLEX_URL")
170+
plex_token = os.getenv("PLEX_TOKEN")
171+
172+
if not plex_url or not plex_token:
173+
return []
174+
175+
# Use Plex API /activities endpoint
176+
activities_url = f"{plex_url}/activities"
177+
params = {
178+
'X-Plex-Token': plex_token
179+
}
180+
181+
response = requests.get(activities_url, params=params, timeout=5)
182+
response.raise_for_status()
183+
184+
# Parse XML response (Plex API returns XML by default)
185+
import xml.etree.ElementTree as ET
186+
root = ET.fromstring(response.text)
187+
188+
activities = []
189+
# Find all Activity elements
190+
for activity in root.findall('.//Activity'):
191+
activity_type = activity.get('type', '')
192+
title = activity.get('title', '')
193+
subtitle = activity.get('subtitle', '')
194+
195+
# Parse context if available
196+
context = {}
197+
context_elem = activity.find('Context')
198+
if context_elem is not None:
199+
library_section_id = context_elem.get('librarySectionID')
200+
if library_section_id:
201+
context['librarySectionID'] = library_section_id
202+
203+
activity_data = {
204+
'key': activity.get('key', ''),
205+
'type': activity_type,
206+
'title': title,
207+
'subtitle': subtitle,
208+
'progress': int(activity.get('progress', 0)),
209+
'context': context,
210+
}
211+
212+
# Check if this is a library refresh/scan activity
213+
# Plex uses types like "library.refresh" or similar
214+
if ('refresh' in activity_type.lower() or
215+
'scan' in activity_type.lower() or
216+
'refresh' in title.lower() or
217+
'scan' in title.lower()):
218+
activities.append(activity_data)
219+
220+
return activities
221+
except Exception as e:
222+
logger.warning(f"Error getting Plex activities: {e}")
223+
return []
224+
225+
160226
def is_plex_scanning(section_key: str) -> bool:
161227
"""
162-
Checks if a Plex library section is currently scanning.
228+
Checks if a Plex library section is currently scanning using the activities API.
163229
164230
Args:
165231
section_key: The key/ID of the library section.
@@ -168,22 +234,36 @@ def is_plex_scanning(section_key: str) -> bool:
168234
True if scanning, False otherwise.
169235
"""
170236
try:
171-
plex = get_plex_client()
172-
if not plex:
173-
return False
237+
# First try using the activities API
238+
activities = get_plex_activities()
239+
for activity in activities:
240+
# Check if activity is related to this section
241+
context = activity.get('context', {})
242+
if isinstance(context, dict):
243+
section_id = context.get('librarySectionID')
244+
if section_id and str(section_id) == str(section_key):
245+
logger.info(f"Found scanning activity for section {section_key}")
246+
return True
174247

175-
# Get the section and check if it's refreshing
176-
section = plex.library.sectionByID(section_key)
177-
if not section:
178-
return False
248+
# Fallback: check section refreshing attribute
249+
plex = get_plex_client()
250+
if plex:
251+
section = plex.library.sectionByID(section_key)
252+
if section:
253+
return getattr(section, 'refreshing', False)
179254

180-
# Check the refreshing attribute (if available)
181-
# Note: Plex API may not always expose this, so we'll use a timeout-based approach
182-
# For now, we'll assume scanning is in progress if we just triggered it
183-
# A better approach is to poll the section's update status
184-
return getattr(section, 'refreshing', False)
255+
return False
185256
except Exception as e:
186257
logger.warning(f"Error checking scan status: {e}")
258+
# Fallback to section attribute check
259+
try:
260+
plex = get_plex_client()
261+
if plex:
262+
section = plex.library.sectionByID(section_key)
263+
if section:
264+
return getattr(section, 'refreshing', False)
265+
except:
266+
pass
187267
return False
188268

189269

0 commit comments

Comments
 (0)