自动化羽毛球比赛提醒

自动化羽毛球比赛提醒

自2025年初以来,我开始更频繁地打羽毛球,几乎每周4-5天。我最近搬到了城市的一个新区域,这意味着我不能再和老朋友们一起打球了。PlayO在帮助我找到新朋友一起打球方面非常有用。在PlayO上,一个主持人可以创建一个比赛,最多6个人可以加入一个场地,进行一个小时的羽毛球双打比赛。

然而,在忙碌的日子里,我常常忘记查看羽毛球比赛,结果发现比赛已经全部预订满了。我想通过创建一个小脚本来自动化这个过程,该脚本会向我发送关于当天比赛可用性的定期提醒,以便我能够在它们被预订完之前预订场地。我从Matt的帖子中获得了灵感,他做了类似的事情。

幸运的是,PlayO有一个公共API端点,可以检索可用比赛的列表:https://api.playo.io/activity-public/list/location

你可以向这个URL发送一个POST请求,并使用这些参数进行过滤:

{
  "lat": 12.9783692,
  "lng": 77.6408356,
  "cityRadius": 5,
  "gameTimeActivities": false,
  "page": 0,
  "lastId": "",
  "sportId": ["SP5"],
  "booking": false,
  "date": ["2025-03-04T11:03:17.260Z"]
}

它返回一个符合这些过滤条件的活动列表。其中一个活动看起来像这样:

{
  "userInfo": [
    {
      "profilePicUrl": "https://playov2.gumlet.io/profiles/redacted.511716.jpg",
      "fName": "Redacted",
      "lName": "",
      "karma": 2800
    },
    {
      "profilePicUrl": "https://playov2.gumlet.io/profiles/redacted-redacted.png",
      "fName": "redacted",
      "lName": "N",
      "karma": 499
    }
  ],
  "isPlayoGame": false,
  "skill": "Intermediate & above",
  "sportName": "Badminton",
  "shortListed": false,
  "joineeList": [
    "7f3cf298-3324-4fc2-96ad-b0f00093cd8f",
    "250572a2-555d-4a77-94f0-452142c08f81",
    "cc3b9eb6-a3b5-4c26-8605-0486fa000a4b",
    "8d5d4299-950b-4011-a7ac-b466b1c00e84",
    "235ae56d-6f4f-4106-9304-fb38e7d4add8"
  ],
  "isPlaypalPlaying": false,
  "lat": 12.976394040119704,
  "lng": 77.63644146986815,
  "location": "Game Theory - Double Road Indiranagar, Indiranagar",
  "joineeCount": 6,
  "status": -1,
  "sportsPlayingMode": {
    "name": "",
    "icon": ""
  },
  "maxPlayers": 7,
  "full": false,
  "price": 0,
  "startTime": "2025-03-04T13:30:00.000Z",
  "endTime": "2025-03-04T15:30:00.000Z",
  "minSkill": 3,
  "maxSkill": 5,
  "skillSet": true,
  "booking": false,
  "bookingId": "",
  "type": 0,
  "venueId": "82af038f-058c-4b2f-bc3d-3a47910d4f97",
  "venueName": "Game Theory - Double Road Indiranagar, Indiranagar",
  "activityType": "regular",
  "isOnline": false,
  "groupId": "",
  "groupName": "",
  "currencyTxt": "INR",
  "strictSkill": true,
  "date": "2025-03-04T00:00:00.000Z",
  "hostId": "redacted",
  "sportId": "SP5",
  "timing": 2,
  "id": "e2ee9f62-c9b6-472b-aea2-b0c52dd7c525",
  "distance": 0.5249236963063415,
  "courtInfo": "",
  "sponsored": false,
  "groups": []
}

使用上述响应,我筛选出以下条件的比赛:

  • fullfalse(这表明joineeCount == maxPlayer不成立,意味着仍有空位可供加入)
  • startTimeendTime在印度标准时间7-8PM之间

我还想添加一个功能,将这些细节发送到Telegram,以便方便地接收通知。然后我与Claude 3.7一起即兴创作,创建了一个Python脚本来自动化整个过程。令人印象深刻的是,它几乎在一次提示中就生成了一个可以工作的脚本,尽管我不得不进行一些小的调整。我相当喜欢Simon Willison使用uv来构建一次性工具的方法。管理依赖项、虚拟环境等仍然是Python的一个痛点,但使用uv相比之下感觉像是魔法。

# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "click",
#     "requests",
#     "pytz",
#     "rich",
#     "python-dateutil",
#     "python-telegram-bot",
# ]
# ///

import click
import requests
import json
import datetime
import pytz
import os
import sys
from rich.console import Console
from rich.table import Table
from dateutil import parser
from telegram import Bot, InputMediaPhoto
from telegram.constants import ParseMode
from io import BytesIO
import asyncio

console = Console()

@click.command()
@click.option("--lat", default=12.9783692, help="Latitude for search")
@click.option("--lng", default=77.6408356, help="Longitude for search")
@click.option("--radius", default=50, help="City radius in km")
@click.option("--sport", default="SP5", help="Sport ID (default: SP5 for Badminton)")
@click.option("--start-time", default="19:00", help="Desired start time (24-hour format HH:MM)")
@click.option("--end-time", default="20:00", help="Desired end time (24-hour format HH:MM)")
@click.option("--timezone", default="Asia/Kolkata", help="Your timezone")
@click.option("--verbose", is_flag=True, help="Show detailed information including exact UTC/IST times")
@click.option("--include-full", is_flag=True, help="Include games that are full")
@click.option("--telegram", is_flag=True, help="Send results to Telegram")
@click.option("--telegram-token", envvar="TELEGRAM_BOT_TOKEN", help="Telegram Bot Token (or set TELEGRAM_BOT_TOKEN env var)")
@click.option("--telegram-chat-id", envvar="TELEGRAM_CHAT_ID", help="Telegram Chat ID (or set TELEGRAM_CHAT_ID env var)")
def find_games(lat, lng, radius, sport, start_time, end_time, timezone, verbose, include_full, telegram, telegram_token, telegram_chat_id):
    """Find available badminton games on Playo matching your criteria."""
    # Get today's date in the specified timezone
    local_tz = pytz.timezone(timezone)
    now = datetime.datetime.now(local_tz)
    today_date = now.strftime("%Y-%m-%dT%H:%M:%S.%fZ")

    # Parse desired time window
    try:
        desired_start = datetime.datetime.strptime(start_time, "%H:%M").time()
        desired_end = datetime.datetime.strptime(end_time, "%H:%M").time()
    except ValueError:
        console.print("[bold red]Error:[/bold red] Invalid time format. Please use HH:MM (24-hour format).")
        return

    console.print(f"[bold green]Searching for badminton games around your location...[/bold green]")
    console.print(f"Looking for games between [bold]{start_time}[/bold] and [bold]{end_time}[/bold] IST today")

    if verbose:
        console.print(f"[dim]Search parameters: lat={lat}, lng={lng}, radius={radius}km[/dim]")
        console.print(f"[dim]Current time in {timezone}: {now.strftime('%Y-%m-%d %H:%M:%S')}[/dim]")

    # Prepare API request
    url = "https://api.playo.io/activity-public/list/location"
    payload = {
        "lat": lat,
        "lng": lng,
        "cityRadius": radius,
        "gameTimeActivities": False,
        "page": 0,
        "lastId": "",
        "sportId": [sport],
        "booking": False,
        "date": [today_date]
    }

    headers = {
        "Content-Type": "application/json",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
    }

    try:
        response = requests.post(url, headers=headers, json=payload)
        response.raise_for_status()
        data = response.json()

        if data.get("requestStatus") != 1 or "data" not in data:
            console.print("[bold red]Error:[/bold red] Failed to get valid response from Playo API")
            return

        # Process activities
        activities = data["data"].get("activities", [])
        if not activities:
            console.print("[yellow]No badminton activities found for today[/yellow]")
            return

        # Filter activities based on criteria
        matching_games = []

        for activity in activities:
            # Convert UTC times to local timezone
            start_time_utc = parser.parse(activity["startTime"])
            end_time_utc = parser.parse(activity["endTime"])

            start_time_local = start_time_utc.astimezone(local_tz)
            end_time_local = end_time_utc.astimezone(local_tz)

            # Print all times in debug mode
            # console.print(f"DEBUG: {activity.get('location', 'Unknown')} - Start: {start_time_local.strftime('%H:%M')} IST (UTC: {start_time_utc.strftime('%H:%M')})")

            # Convert time objects correctly for comparison
            start_hour = start_time_local.hour
            start_minute = start_time_local.minute

            # Convert desired times to hours and minutes for easier comparison
            desired_start_hour = desired_start.hour
            desired_start_minute = desired_start.minute
            desired_end_hour = desired_end.hour
            desired_end_minute = desired_end.minute

            # Check if this game starts at 7PM (19:00) and ends at 8PM (20:00)
            is_time_match = False

            # Get duration in minutes
            duration_minutes = ((end_time_local.hour * 60 + end_time_local.minute) -
                               (start_time_local.hour * 60 + start_time_local.minute))

            # Check if start time is 7PM (with small tolerance)
            if (start_hour == desired_start_hour and
                start_minute >= desired_start_minute and
                start_minute < desired_start_minute + 10):  # Allow a small window of 10 minutes

                # Check if duration is approximately 1 hour (between 50-70 minutes)
                if 50 <= duration_minutes <= 70:
                    is_time_match = True

            # Check if there are available slots
            is_available = (
                not activity.get("full", True) and
                (activity.get("maxPlayers", 0) == -1 or
                 activity.get("joineeCount", 0) < activity.get("maxPlayers", 0))
            )

            # When verbose, print time details for each game to help debug
            if verbose:
                time_info = f"[dim]{activity.get('location', 'Unknown')} - Start: {start_time_local.strftime('%H:%M')} IST ({start_time_utc.strftime('%H:%M')} UTC), " + \
                           f"End: {end_time_local.strftime('%H:%M')} IST, Duration: {duration_minutes} min, " + \
                           f"Time match: {'Yes' if is_time_match else 'No'}, Available: {'Yes' if is_available else 'No'}[/dim]"
                console.print(time_info)

            # Both conditions must be true
            if is_time_match and is_available:
                matching_games.append({
                    "id": activity["id"],
                    "location": activity["location"],
                    "venue_name": activity.get("venueName", "N/A"),
                    "start": start_time_local.strftime("%I:%M %p"),
                    "end": end_time_local.strftime("%I:%M %p"),
                    "players": f"{activity.get('joineeCount', 0)}/{activity.get('maxPlayers', 'unlimited')}",
                    "host": activity.get("userInfo", [{}])[0].get("fName", "Unknown"),
                    "skill": activity.get("skill", "Any"),
                    "price": f"{activity.get('price', 0)} {activity.get('currencyTxt', 'INR')}"
                })

        # Display results
        if matching_games:
            table = Table(title=f"Available Badminton Games ({len(matching_games)} matches found)")

            table.add_column("Location", style="cyan")
            table.add_column("Time", style="green")
            table.add_column("Players", style="yellow")
            table.add_column("Host", style="magenta")
            table.add_column("Skill Level", style="blue")
            table.add_column("Link", style="bright_blue")

            for game in matching_games:
                table.add_row(
                    f"{game['venue_name']}",
                    f"{game['start']} - {game['end']}",
                    game["players"],
                    game["host"],
                    game["skill"],
                    f"https://playo.co/match/{game['id']}"
                )

            console.print(table)

            # Send to Telegram if requested
            if telegram:
                if not telegram_token or not telegram_chat_id:
                    console.print("[bold red]Error:[/bold red] Telegram token and chat ID are required for Telegram notifications")
                    console.print("[dim]Set them with --telegram-token and --telegram-chat-id or via environment variables[/dim]")
                else:
                    try:
                        send_to_telegram(matching_games, telegram_token, telegram_chat_id)
                        console.print("[green]Results sent to Telegram successfully![/green]")
                    except Exception as e:
                        console.print(f"[bold red]Error sending to Telegram:[/bold red] {e}")
        else:
            console.print("[yellow]No games found matching your criteria[/yellow]")
            if telegram and telegram_token and telegram_chat_id:
                try:
                    asyncio.run(send_telegram_message(
                        "No badminton games found matching your criteria for today.",
                        telegram_token,
                        telegram_chat_id
                    ))
                    console.print("[green]Empty results notification sent to Telegram[/green]")
                except Exception as e:
                    console.print(f"[bold red]Error sending to Telegram:[/bold red] {e}")

    except requests.RequestException as e:
        console.print(f"[bold red]Error:[/bold red] Failed to connect to Playo API: {e}")
    except json.JSONDecodeError:
        console.print("[bold red]Error:[/bold red] Failed to parse API response")
    except Exception as e:
        console.print(f"[bold red]Error:[/bold red] An unexpected error occurred: {e}")

def send_to_telegram(games, token, chat_id):
    """Send game information to Telegram as a nicely formatted message."""
    if not games:
        return

    # Create a formatted message for Telegram
    message = "🏸 *Available Badminton Games* 🏸\n\n"

    for i, game in enumerate(games, 1):
        message += f"*{i}. {game['venue_name']}*\n"
        message += f"⏰ {game['start']} - {game['end']}\n"
        message += f"👥 Players: {game['players']}\n"
        message += f"👤 Host: {game['host']}\n"
        message += f"🎯 Skill: {game['skill']}\n"
        message += f"🔗 [Join Game](https://playo.co/match/{game['id']})\n\n"

    # Send the message
    asyncio.run(send_telegram_message(message, token, chat_id))

async def send_telegram_message(message, token, chat_id):
    """Send a message to Telegram using the Bot API."""
    bot = Bot(token=token)
    await bot.send_message(
        chat_id=chat_id,
        text=message,
        parse_mode=ParseMode.MARKDOWN,
        disable_web_page_preview=False
    )

if __name__ == "__main__":
    find_games()

该脚本输出一个美观的输出:

img

Telegram:

img

定时任务

我希望这个脚本每天都能可靠地运行,因此我使用了GitHub Actions来实现这一点。GitHub Actions感觉是最简单的路径,因为我无需担心保持

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值