33"""
44import asyncio
55import logging
6+ import uuid
67from collections import deque , defaultdict
78from datetime import datetime
89from flask import Flask , request , jsonify , send_from_directory
3435OVERSEERR_USERS_DATA : Dict [str , dict ] = {}
3536NOTIFICATION_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
3840DEBOUNCE_SECONDS = 60
3941
4042app = 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():
822894def 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
0 commit comments