diff --git a/CHANGELOG.md b/CHANGELOG.md index c693d6c..7dfafac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## v3.6 - 改进TBD比赛处理机制 +- **放宽TBD比赛时间匹配条件**: + - 将TBD事件匹配的时间窗口从5分钟扩大到1小时 + - 更好地处理比赛时间调整的情况 + - 添加时间差异显示,方便调试 +- **自动删除过期TBD事件**: + - 新增 `delete_calendar_event()` 方法 + - 自动检测并删除超过2小时的过期TBD事件 + - 避免日历中积累无效的占位事件 +- **增强的TBD事件管理**: + - 跟踪已更新的TBD事件,避免误删 + - 在同步摘要中显示删除的TBD事件数量 + ## v3.5 - 修复TBD比赛标记问题 - **修复TBD比赛错误标记为完成**: - 修改比分解析逻辑,只匹配破折号而不匹配冒号 diff --git a/README.md b/README.md index 5b1c02b..8c5b38d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Dota 2 Calendar Sync v3.5 +# Dota 2 Calendar Sync v3.6 -自动从 Liquipedia 获取 Dota 2 Tier 1 比赛信息并同步到 Google Calendar,支持自动更新比赛结果和时间变更。 +自动从 Liquipedia 获取 Dota 2 Tier 1 比赛信息并同步到 Google Calendar,支持自动更新比赛结果、时间变更和智能管理TBD占位事件。 ## 功能 @@ -9,6 +9,7 @@ - 自动创建 Google Calendar 事件 - **自动更新已完成比赛的结果和比分** - **检测并更新比赛时间变更**(赛程调整时自动同步) +- **智能管理TBD占位事件**(自动更新队伍信息,删除过期事件) - 避免重复添加已存在的比赛 - 支持 dry-run 模式进行测试 @@ -82,6 +83,7 @@ python sync_dota2_matches.py --dry-run - 提取比赛格式(Bo1、Bo3、Bo5) - **智能去重**:相同时间、相同轮次的 TBD 比赛只保留一个代表 - **TBD比赛保护**:确保TBD vs TBD的比赛不会被错误标记为已完成 + - **改进的TBD匹配**:放宽时间匹配窗口至1小时,更好处理赛程调整 2. **日历事件管理**: - 自动设置比赛时长(根据 Bo 格式估算) @@ -91,6 +93,7 @@ python sync_dota2_matches.py --dry-run - **自动更新已完成比赛的结果** - **在标题添加完成标记(✓ + 比分)** - **TBD 比赛队伍确定后自动更新** + - **自动清理过期的TBD占位事件** 3. **错误处理**: - 网络请求超时处理 diff --git a/cleanup_duplicates.py b/cleanup_duplicates.py deleted file mode 100644 index 6df73ed..0000000 --- a/cleanup_duplicates.py +++ /dev/null @@ -1,241 +0,0 @@ -#!/usr/bin/env python3 -""" -Cleanup duplicate calendar events -Finds and optionally removes duplicate Dota 2 match events -""" - -import requests -from google.oauth2 import service_account -from googleapiclient.discovery import build -from datetime import datetime, timedelta -import pytz -import re -import sys -import argparse - -def authenticate(credentials_file='credentials.json'): - """Authenticate with Google Calendar using service account credentials""" - try: - credentials = service_account.Credentials.from_service_account_file( - credentials_file, - scopes=['https://www.googleapis.com/auth/calendar'] - ) - service = build('calendar', 'v3', credentials=credentials) - print(f"✓ Successfully authenticated with Google Calendar") - return service - except Exception as e: - print(f"✗ Authentication failed: {e}") - sys.exit(1) - -def find_duplicates(service, calendar_id='primary', days_back=7, days_ahead=30): - """Find duplicate events in the calendar""" - try: - now = datetime.utcnow() - time_min = (now - timedelta(days=days_back)).isoformat() + 'Z' - time_max = (now + timedelta(days=days_ahead)).isoformat() + 'Z' - - print(f"Scanning calendar from {days_back} days ago to {days_ahead} days ahead...") - - events_result = service.events().list( - calendarId=calendar_id, - timeMin=time_min, - timeMax=time_max, - maxResults=500, - singleEvents=True, - orderBy='startTime' - ).execute() - - events = events_result.get('items', []) - - # Group events by match key (teams + tournament + time window) - matches = {} - - for event in events: - summary = event.get('summary', '') - - # Skip non-Dota events - if 'vs' not in summary: - continue - - # Extract teams and tournament - # Remove score and checkmark - clean_summary = re.sub(r'^✓?\s*\d+[-:]\d+\s*', '', summary) - clean_summary = re.sub(r'\[COMPLETED\]\s*', '', clean_summary) - - # Extract teams - match = re.search(r'^(.*?)\s+vs\s+(.*?)(?:\s*\[(.*?)\])?$', clean_summary) - if not match: - continue - - team1 = match.group(1).strip() - team2 = match.group(2).strip() - tournament = match.group(3).strip() if match.group(3) else '' - - # Get event time - event_start = event['start'].get('dateTime', event['start'].get('date')) - event_dt = datetime.fromisoformat(event_start.replace('Z', '+00:00')) - - # Create match key (teams + tournament + time rounded to hour) - time_key = event_dt.strftime('%Y-%m-%d-%H') - match_key = f"{sorted([team1, team2])}_{tournament}_{time_key}" - - if match_key not in matches: - matches[match_key] = [] - - matches[match_key].append({ - 'id': event['id'], - 'summary': summary, - 'start': event_start, - 'team1': team1, - 'team2': team2, - 'tournament': tournament, - 'created': event.get('created', ''), - 'updated': event.get('updated', ''), - 'description': event.get('description', '') - }) - - # Find duplicates - duplicates = {} - for match_key, events in matches.items(): - if len(events) > 1: - duplicates[match_key] = events - - return duplicates - - except Exception as e: - print(f"✗ Error scanning calendar: {e}") - return {} - -def display_duplicates(duplicates): - """Display found duplicates""" - if not duplicates: - print("\n✓ No duplicate events found!") - return - - print(f"\n⚠️ Found {len(duplicates)} sets of duplicate events:") - print("=" * 80) - - for match_key, events in duplicates.items(): - print(f"\nDuplicate set: {len(events)} events") - print("-" * 40) - - for i, event in enumerate(events, 1): - print(f"\nEvent #{i}:") - print(f" Summary: {event['summary']}") - print(f" Teams: {event['team1']} vs {event['team2']}") - print(f" Tournament: {event['tournament']}") - print(f" Time: {event['start'][:19]}") - print(f" Event ID: {event['id']}") - print(f" Created: {event['created'][:19] if event['created'] else 'N/A'}") - print(f" Updated: {event['updated'][:19] if event['updated'] else 'N/A'}") - - # Check if has result - if '✓' in event['summary'] or '🏆 RESULT' in event['description']: - print(f" Status: COMPLETED") - elif '📊 CURRENT SCORE' in event['description']: - print(f" Status: IN PROGRESS") - else: - print(f" Status: UPCOMING") - -def remove_duplicates(service, duplicates, calendar_id='primary', dry_run=True): - """Remove duplicate events, keeping the most recently updated one""" - if not duplicates: - return - - print("\n" + "=" * 80) - print("Duplicate Removal Process") - print("=" * 80) - - removed_count = 0 - - for match_key, events in duplicates.items(): - print(f"\nProcessing duplicate set with {len(events)} events...") - - # Sort by updated time (keep most recent) - events.sort(key=lambda x: x.get('updated', x.get('created', '')), reverse=True) - - # Keep the first (most recent) event - keep_event = events[0] - remove_events = events[1:] - - print(f" Keeping: {keep_event['summary']} (updated: {keep_event['updated'][:19] if keep_event['updated'] else 'N/A'})") - - for event in remove_events: - if dry_run: - print(f" ◯ Would remove: {event['summary']} (ID: {event['id'][:20]}...)") - removed_count += 1 - else: - try: - service.events().delete( - calendarId=calendar_id, - eventId=event['id'] - ).execute() - print(f" ✓ Removed: {event['summary']} (ID: {event['id'][:20]}...)") - removed_count += 1 - except Exception as e: - print(f" ✗ Failed to remove: {event['summary']} - {e}") - - print("\n" + "=" * 80) - print(f"Summary: {'Would remove' if dry_run else 'Removed'} {removed_count} duplicate events") - if dry_run: - print("⚠️ DRY RUN - No actual changes made. Use --remove to actually remove duplicates") - -def main(): - parser = argparse.ArgumentParser( - description='Find and remove duplicate Dota 2 calendar events' - ) - parser.add_argument( - '--calendar-id', - default='primary', - help='Google Calendar ID (default: primary)' - ) - parser.add_argument( - '--remove', - action='store_true', - help='Actually remove duplicates (default is dry-run)' - ) - parser.add_argument( - '--days-back', - type=int, - default=7, - help='Days to look back (default: 7)' - ) - parser.add_argument( - '--days-ahead', - type=int, - default=30, - help='Days to look ahead (default: 30)' - ) - parser.add_argument( - '--credentials', - default='credentials.json', - help='Path to Google service account credentials JSON file' - ) - - args = parser.parse_args() - - # Authenticate - service = authenticate(args.credentials) - - # Find duplicates - duplicates = find_duplicates( - service, - calendar_id=args.calendar_id, - days_back=args.days_back, - days_ahead=args.days_ahead - ) - - # Display duplicates - display_duplicates(duplicates) - - # Remove duplicates if requested - if duplicates: - remove_duplicates( - service, - duplicates, - calendar_id=args.calendar_id, - dry_run=not args.remove - ) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/delete_duplicates.py b/delete_duplicates.py deleted file mode 100644 index e9a1864..0000000 --- a/delete_duplicates.py +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env python3 -""" -Delete duplicate calendar events based on screenshot -Manually delete the duplicate events that were created during live score updates -""" - -from google.oauth2 import service_account -from googleapiclient.discovery import build -from datetime import datetime, timedelta -import pytz -import sys - -def authenticate(credentials_file='credentials.json'): - """Authenticate with Google Calendar using service account credentials""" - try: - credentials = service_account.Credentials.from_service_account_file( - credentials_file, - scopes=['https://www.googleapis.com/auth/calendar'] - ) - service = build('calendar', 'v3', credentials=credentials) - print(f"✓ Successfully authenticated with Google Calendar") - return service - except Exception as e: - print(f"✗ Authentication failed: {e}") - sys.exit(1) - -def delete_specific_events(service, calendar_id='primary'): - """Delete specific duplicate events based on partial scores""" - - # Based on the screenshot, these are the duplicate events to look for: - duplicates_to_find = [ - ("1-0", "NGX", "Liquid"), # 19:00 1-0 NGX vs Liquid - ("1-1", "NGX", "Liquid"), # 19:45 1-1 NGX vs Liquid - ] - - # Get events for Sept 5 - target_date = datetime(2025, 9, 5, tzinfo=pytz.UTC) - time_min = target_date.isoformat() - time_max = (target_date + timedelta(days=1)).isoformat() - - events_result = service.events().list( - calendarId=calendar_id, - timeMin=time_min, - timeMax=time_max, - maxResults=500, - singleEvents=True, - orderBy='startTime' - ).execute() - - events = events_result.get('items', []) - - print(f"\nScanning {len(events)} events on September 5th...") - print("=" * 60) - - events_to_delete = [] - - for event in events: - summary = event.get('summary', '') - - # Check if this matches any of our duplicate patterns - for score, team1, team2 in duplicates_to_find: - if score in summary and team1 in summary and team2 in summary and '✓' not in summary: - events_to_delete.append({ - 'id': event['id'], - 'summary': summary, - 'start': event['start'].get('dateTime', event['start'].get('date')) - }) - print(f"Found duplicate: {summary}") - print(f" Time: {event['start'].get('dateTime', event['start'].get('date'))}") - print(f" ID: {event['id']}") - break - - if not events_to_delete: - print("\n❌ No duplicate events found in API.") - print("This could mean:") - print("1. The duplicates are in a different calendar") - print("2. They have already been deleted") - print("3. The calendar interface is showing cached data") - print("\nTry refreshing your browser (Ctrl+F5 or Cmd+Shift+R)") - return - - print(f"\n⚠️ Found {len(events_to_delete)} duplicate events to delete") - print("-" * 60) - - # Confirm before deleting - print("\nThese events will be deleted:") - for event in events_to_delete: - print(f" - {event['summary']} at {event['start'][:19]}") - - response = input("\nDo you want to delete these events? (yes/no): ") - - if response.lower() == 'yes': - deleted_count = 0 - for event in events_to_delete: - try: - service.events().delete( - calendarId=calendar_id, - eventId=event['id'] - ).execute() - print(f"✓ Deleted: {event['summary']}") - deleted_count += 1 - except Exception as e: - print(f"✗ Failed to delete {event['summary']}: {e}") - - print(f"\n✓ Successfully deleted {deleted_count} duplicate events") - else: - print("\n❌ Deletion cancelled") - -def main(): - # Authenticate - service = authenticate() - - # Delete specific duplicates - delete_specific_events(service) - - print("\nPlease refresh your Google Calendar to see the changes.") - print("If you still see duplicates, they might be in a different calendar.") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/sync_dota2_matches.py b/sync_dota2_matches.py index e8b4c23..e05082e 100644 --- a/sync_dota2_matches.py +++ b/sync_dota2_matches.py @@ -695,7 +695,19 @@ class Dota2CalendarSync: return diff >= 5 # Return True if difference is 5 minutes or more - def sync_matches_to_calendar(self, dry_run=False, update_results=True, update_times=True): + def delete_calendar_event(self, event_id): + """Delete a calendar event""" + try: + self.service.events().delete( + calendarId=self.calendar_id, + eventId=event_id + ).execute() + return True + except Exception as e: + print(f"Error deleting event: {e}") + return False + + def sync_matches_to_calendar(self, dry_run=False, update_results=True, update_times=True, delete_old_tbd=True): """Main sync function with time updates""" print("\n" + "="*50) print("Starting Dota 2 Calendar Sync v3") @@ -717,6 +729,10 @@ class Dota2CalendarSync: updated_count = 0 time_updated_count = 0 error_count = 0 + deleted_tbd_count = 0 + + # Track which TBD events were updated + updated_tbd_events = set() # Process upcoming matches print("\nProcessing upcoming matches...") @@ -770,9 +786,11 @@ class Dota2CalendarSync: # Check if time matches event_start = event['start'].get('dateTime', event['start'].get('date')) event_dt = datetime.fromisoformat(event_start.replace('Z', '+00:00')) - if abs((event_dt - match_time).total_seconds()) < 300: # Within 5 minutes + # Relaxed time matching: within 1 hour (3600 seconds) + if abs((event_dt - match_time).total_seconds()) < 3600: # Within 1 hour existing_event = event print(f" → Found TBD match to update: {team1} vs {team2}") + print(f" Time difference: {abs((event_dt - match_time).total_seconds())/60:.0f} minutes") break if existing_event: @@ -789,6 +807,7 @@ class Dota2CalendarSync: if self.update_event_with_teams(existing_event['id'], match): print(f"✓ Updated TBD match with teams: {team1} vs {team2}") updated_count += 1 + updated_tbd_events.add(existing_event['id']) time.sleep(0.2) else: print(f"✗ Failed to update TBD match: {team1} vs {team2}") @@ -941,6 +960,42 @@ class Dota2CalendarSync: print(f"✗ Failed to update: {team1} vs {team2}") error_count += 1 + # Delete old TBD events that are past and not updated + if delete_old_tbd and not dry_run: + print("\nChecking for expired TBD events to delete...") + print("-" * 30) + + # Get all TBD events again to check which ones to delete + for key, event in existing_events.items(): + if key == '_by_match': + continue + + summary = event.get('summary', '') + if 'TBD vs TBD' in summary: + event_id = event['id'] + + # Skip if this event was updated + if event_id in updated_tbd_events: + continue + + # Check if event is in the past + event_start = event['start'].get('dateTime', event['start'].get('date')) + event_dt = datetime.fromisoformat(event_start.replace('Z', '+00:00')) + + # If event is more than 2 hours in the past, delete it + if event_dt < now - timedelta(hours=2): + if dry_run: + print(f"◯ Would delete expired TBD event: {summary} ({event_dt.strftime('%Y-%m-%d %H:%M UTC')})") + deleted_tbd_count += 1 + else: + if self.delete_calendar_event(event_id): + print(f"🗑️ Deleted expired TBD event: {summary} ({event_dt.strftime('%Y-%m-%d %H:%M UTC')})") + deleted_tbd_count += 1 + time.sleep(0.2) + else: + print(f"✗ Failed to delete TBD event: {summary}") + error_count += 1 + # Summary print("\n" + "="*50) print("Sync Summary") @@ -950,6 +1005,8 @@ class Dota2CalendarSync: print(f"⏰ Time updated: {time_updated_count} matches") if updated_count > 0: print(f"✓ Results updated: {updated_count} matches") + if deleted_tbd_count > 0: + print(f"🗑️ Deleted: {deleted_tbd_count} expired TBD events") print(f"⊘ Skipped: {skipped_count} matches (no changes)") if error_count > 0: print(f"✗ Errors: {error_count} matches") @@ -1008,7 +1065,8 @@ def main(): sync.sync_matches_to_calendar( dry_run=args.dry_run, update_results=not args.no_results, - update_times=not args.no_time_updates + update_times=not args.no_time_updates, + delete_old_tbd=True ) except KeyboardInterrupt: