1919import requests
2020import sys
2121from datetime import datetime , timedelta , timezone
22+ from dateutil import parser as date_parser
2223
2324# Resolve paths relative to this script
2425SCRIPT_DIR = os .path .dirname (os .path .abspath (__file__ ))
@@ -258,6 +259,38 @@ def find_expected_missing_calls(mapping, days_ahead=7):
258259 return missing
259260
260261
262+ def extract_date_from_title (title ):
263+ """Extract date from YouTube title using various common patterns.
264+
265+ Returns datetime object or None if no date found.
266+ """
267+ if not title :
268+ return None
269+
270+ # Try various date patterns commonly found in stream titles
271+ patterns = [
272+ # "February 13, 2026" or "Feb 13, 2026"
273+ r'([A-Za-z]+)\s+(\d{1,2}),?\s+(\d{4})' ,
274+ # "2026-02-13" or "2026/02/13"
275+ r'(\d{4})[-/](\d{1,2})[-/](\d{1,2})' ,
276+ # "13 February 2026" or "13 Feb 2026"
277+ r'(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})' ,
278+ ]
279+
280+ for pattern in patterns :
281+ match = re .search (pattern , title )
282+ if match :
283+ try :
284+ # Try to parse the matched string
285+ date_str = match .group (0 )
286+ dt = date_parser .parse (date_str , fuzzy = True )
287+ return dt .replace (tzinfo = timezone .utc )
288+ except (ValueError , TypeError ):
289+ continue
290+
291+ return None
292+
293+
261294def check_warnings (upcoming_calls , youtube_cache , youtube_enabled = True , youtube_error = None , missing_calls = None ):
262295 """Check for potential issues and return a list of warning strings."""
263296 warnings = []
@@ -306,6 +339,18 @@ def check_warnings(upcoming_calls, youtube_cache, youtube_enabled=True, youtube_
306339 f"{ label } : YouTube scheduled time ({ yt_time } ) differs from meeting time ({ call_time } )"
307340 )
308341
342+ # Check: YouTube title contains a stale/mismatched date
343+ if yt ["title" ] and call ["start_time" ]:
344+ title_date = extract_date_from_title (yt ["title" ])
345+ if title_date :
346+ # Compare dates (ignoring time)
347+ title_date_only = title_date .date ()
348+ call_date_only = call ["start_time" ].date ()
349+ if title_date_only != call_date_only :
350+ warnings .append (
351+ f"{ label } : YouTube title contains date { title_date .strftime ('%B %d, %Y' )} but meeting is scheduled for { call ['start_time' ].strftime ('%B %d, %Y' )} - STALE STREAM TITLE"
352+ )
353+
309354 # Check: stream not in "upcoming" status
310355 status = yt .get ("broadcast_status" , "unknown" )
311356 if status == "none" :
@@ -326,11 +371,27 @@ def build_markdown(upcoming_calls, zoom_details_cache, youtube_cache, days_ahead
326371 now_str = datetime .now (timezone .utc ).strftime ("%Y-%m-%d %H:%M UTC" )
327372 lines = []
328373
374+ lines .append (f"#### Upcoming Calls — Next { days_ahead } Days" )
375+ lines .append (f"*Generated: { now_str } *" )
376+
377+ # Warnings at the top
378+ warnings = check_warnings (
379+ upcoming_calls , youtube_cache ,
380+ youtube_enabled = youtube_enabled , youtube_error = youtube_error ,
381+ missing_calls = missing_calls ,
382+ )
383+ if warnings :
384+ lines .append ("" )
385+ lines .append ("---" )
386+ lines .append (f"**:warning: Warnings ({ len (warnings )} )**" )
387+ for w in warnings :
388+ lines .append (f"- { w } " )
389+
329390 if not upcoming_calls :
330- lines .append (f"No upcoming calls found in the next { days_ahead } days (as of { now_str } )." )
391+ lines .append ("" )
392+ lines .append ("---" )
393+ lines .append (f"No upcoming calls found in the next { days_ahead } days." )
331394 else :
332- lines .append (f"#### Upcoming Calls — Next { days_ahead } Days" )
333- lines .append (f"*Generated: { now_str } *" )
334395
335396 for call in upcoming_calls :
336397 # Title linked to issue
@@ -378,19 +439,6 @@ def build_markdown(upcoming_calls, zoom_details_cache, youtube_cache, days_ahead
378439 else :
379440 lines .append ("- **YouTube:** No stream scheduled" )
380441
381- # Warnings
382- warnings = check_warnings (
383- upcoming_calls , youtube_cache ,
384- youtube_enabled = youtube_enabled , youtube_error = youtube_error ,
385- missing_calls = missing_calls ,
386- )
387- if warnings :
388- lines .append ("" )
389- lines .append ("---" )
390- lines .append (f"**:warning: Warnings ({ len (warnings )} )**" )
391- for w in warnings :
392- lines .append (f"- { w } " )
393-
394442 lines .append ("" )
395443 lines .append ("---" )
396444 lines .append (f"*{ len (upcoming_calls )} call(s) in the next { days_ahead } days*" )
@@ -421,15 +469,23 @@ def print_report(upcoming_calls, zoom_details_cache, youtube_cache, days_ahead=7
421469 """Print a plain-text report to the terminal."""
422470 now_str = datetime .now (timezone .utc ).strftime ("%Y-%m-%d %H:%M UTC" )
423471
472+ print (f"{ '=' * 60 } " )
473+ print (f" UPCOMING CALLS - Next { days_ahead } Days" )
474+ print (f" Generated: { now_str } " )
475+ print (f"{ '=' * 60 } " )
476+
477+ # Warnings at the top
478+ warnings = check_warnings (upcoming_calls , youtube_cache , youtube_enabled = youtube_enabled , youtube_error = youtube_error , missing_calls = missing_calls )
479+ if warnings :
480+ print (f"\n { '=' * 60 } " )
481+ print (f" WARNINGS ({ len (warnings )} )" )
482+ print (f"{ '=' * 60 } " )
483+ for w in warnings :
484+ print (f" ! { w } " )
485+
424486 if not upcoming_calls :
425- print (
426- f"No upcoming calls found in the next { days_ahead } days (as of { now_str } )."
427- )
487+ print (f"\n No upcoming calls found in the next { days_ahead } days." )
428488 else :
429- print (f"{ '=' * 60 } " )
430- print (f" UPCOMING CALLS - Next { days_ahead } Days" )
431- print (f" Generated: { now_str } " )
432- print (f"{ '=' * 60 } " )
433489
434490 for call in upcoming_calls :
435491 # Flag first occurrence of a new call series
@@ -475,15 +531,6 @@ def print_report(upcoming_calls, zoom_details_cache, youtube_cache, days_ahead=7
475531 else :
476532 print (f" YouTube: No stream scheduled" )
477533
478- # Warnings section
479- warnings = check_warnings (upcoming_calls , youtube_cache , youtube_enabled = youtube_enabled , youtube_error = youtube_error , missing_calls = missing_calls )
480- if warnings :
481- print (f"\n { '=' * 60 } " )
482- print (f" WARNINGS ({ len (warnings )} )" )
483- print (f"{ '=' * 60 } " )
484- for w in warnings :
485- print (f" ! { w } " )
486-
487534 print (f"\n { '=' * 60 } " )
488535 print (f" Total: { len (upcoming_calls )} call(s) in the next { days_ahead } days" )
489536 print (f"{ '=' * 60 } " )
0 commit comments