310 lines
13 KiB
Python
310 lines
13 KiB
Python
# Native modules
|
|
import logging
|
|
import traceback
|
|
import os
|
|
import time
|
|
from datetime import datetime, timedelta, date
|
|
|
|
# Third-party modules
|
|
import requests
|
|
from dateutil.parser import parse
|
|
import pytz
|
|
from dotenv import load_dotenv
|
|
from urllib.parse import urlencode
|
|
from typing import List, Dict, Optional, Any, Tuple
|
|
|
|
# Import the SessionNotifier class
|
|
from session_notifier import SessionNotifier
|
|
|
|
# Import the preferred sessions from the session_config module
|
|
from session_config import PREFERRED_SESSIONS
|
|
|
|
# Import the AuthHandler class
|
|
from auth import AuthHandler
|
|
|
|
load_dotenv()
|
|
|
|
# Configuration
|
|
USERNAME = os.environ.get("CROSSFIT_USERNAME")
|
|
PASSWORD = os.environ.get("CROSSFIT_PASSWORD")
|
|
|
|
if not all([USERNAME, PASSWORD]):
|
|
raise ValueError("Missing environment variables: CROSSFIT_USERNAME and/or CROSSFIT_PASSWORD")
|
|
|
|
APPLICATION_ID = "81560887"
|
|
CATEGORY_ID = "677" # Activity category ID for CrossFit
|
|
TIMEZONE = "Europe/Paris" # Adjust to your timezone
|
|
# Booking window configuration (can be overridden by environment variables)
|
|
# TARGET_RESERVATION_TIME: string "HH:MM" local time when bookings open (default 20:01)
|
|
# BOOKING_WINDOW_END_DELTA_MINUTES: int minutes after target time to stop booking (default 10)
|
|
TARGET_RESERVATION_TIME = os.environ.get("TARGET_RESERVATION_TIME", "20:01")
|
|
BOOKING_WINDOW_END_DELTA_MINUTES = int(os.environ.get("BOOKING_WINDOW_END_DELTA_MINUTES", "10"))
|
|
DEVICE_TYPE = "3"
|
|
|
|
# Retry configuration
|
|
RETRY_MAX = 3
|
|
RETRY_BACKOFF = 1
|
|
APP_VERSION = "5.09.21"
|
|
|
|
class Booker:
|
|
"""
|
|
A class for handling the main booking logic.
|
|
This class is designed to be used as a standalone component
|
|
that can be initialized with authentication and session management
|
|
and used to perform the booking process.
|
|
"""
|
|
|
|
def __init__(self, auth_handler: AuthHandler, notifier: SessionNotifier) -> None:
|
|
"""
|
|
Initialize the Booker with necessary attributes.
|
|
|
|
Args:
|
|
auth_handler (AuthHandler): AuthHandler instance for authentication.
|
|
notifier (SessionNotifier): SessionNotifier instance for sending notifications.
|
|
"""
|
|
self.auth_handler = auth_handler
|
|
self.notifier = notifier
|
|
|
|
# Initialize the session and headers
|
|
self.session: requests.Session = requests.Session()
|
|
self.base_headers: Dict[str, str] = {
|
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:140.0) Gecko/20100101 Firefox/140.0",
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
"Nubapp-Origin": "user_apps",
|
|
}
|
|
self.session.headers.update(self.base_headers)
|
|
|
|
# Define mandatory parameters for API calls
|
|
self.mandatory_params: Dict[str, str] = {
|
|
"app_version": APP_VERSION,
|
|
"device_type": DEVICE_TYPE,
|
|
"id_application": APPLICATION_ID,
|
|
"id_category_activity": CATEGORY_ID
|
|
}
|
|
|
|
def get_auth_headers(self) -> Dict[str, str]:
|
|
"""
|
|
Return headers with authorization from the AuthHandler.
|
|
|
|
Returns:
|
|
Dict[str, str]: Headers dictionary with authorization if available.
|
|
"""
|
|
return self.auth_handler.get_auth_headers()
|
|
|
|
async def booker(self, current_time: datetime) -> None:
|
|
"""
|
|
Run one cycle of checking and booking sessions.
|
|
|
|
Args:
|
|
current_time (datetime): Current time for comparison.
|
|
"""
|
|
# Calculate date range to check (current day, day + 1, and day + 2)
|
|
start_date: date = current_time.date()
|
|
end_date: date = start_date + timedelta(days=2) # Only go up to day + 2
|
|
|
|
# Get available sessions
|
|
sessions_data: Optional[Dict[str, Any]] = self.get_available_sessions(start_date, end_date)
|
|
if not sessions_data or not sessions_data.get("success", False):
|
|
logging.error("No sessions available or error fetching sessions")
|
|
return
|
|
|
|
activities: List[Dict[str, Any]] = sessions_data.get("data", {}).get("activities_calendar", [])
|
|
|
|
# Display all available sessions within the date range
|
|
self.display_upcoming_sessions(activities, current_time)
|
|
|
|
# Find sessions to book (preferred only) within allowed date range
|
|
found_preferred_sessions: List[Dict[str, Any]] = []
|
|
|
|
for session in activities:
|
|
session_time: datetime = parse(session["start_timestamp"])
|
|
if not session_time.tzinfo:
|
|
session_time = pytz.timezone(TIMEZONE).localize(session_time)
|
|
|
|
# Check if session is within allowed date range (current day, day + 1, or day + 2)
|
|
days_diff = (session_time.date() - current_time.date()).days
|
|
if not (0 <= days_diff <= 2):
|
|
continue # Skip sessions outside the allowed date range
|
|
|
|
# Check if session is preferred and bookable
|
|
if self.is_session_bookable(session, current_time):
|
|
if self.matches_preferred_session(session, current_time):
|
|
found_preferred_sessions.append(session)
|
|
|
|
# Display preferred sessions found
|
|
if found_preferred_sessions:
|
|
logging.info("Preferred sessions found:")
|
|
for session in found_preferred_sessions:
|
|
session_time: datetime = parse(session["start_timestamp"])
|
|
if not session_time.tzinfo:
|
|
session_time = pytz.timezone(TIMEZONE).localize(session_time)
|
|
logging.info(f"ID: {session['id_activity_calendar']}, Name: {session['name_activity']}, Time: {session_time.strftime('%Y-%m-%d %H:%M')}")
|
|
else:
|
|
logging.info("No matching preferred sessions found")
|
|
|
|
# Book preferred sessions
|
|
if not found_preferred_sessions:
|
|
logging.info("No matching sessions found to book")
|
|
return
|
|
|
|
# Book sessions (preferred first)
|
|
sessions_to_book = [("Preferred", session) for session in found_preferred_sessions]
|
|
sessions_to_book.sort(key=lambda x: 0 if x[0] == "Preferred" else 1)
|
|
booked_sessions = []
|
|
|
|
for session_type, session in sessions_to_book:
|
|
session_time: datetime = parse(session["start_timestamp"])
|
|
logging.info(f"Attempting to book {session_type} session at {session_time} ({session['name_activity']})")
|
|
if self.book_session(session["id_activity_calendar"]):
|
|
# Display booked session
|
|
booked_sessions.append(session)
|
|
logging.info(f"Successfully booked {session_type} session at {session_time}")
|
|
|
|
# Notify about booked session
|
|
session_details = f"{session['name_activity']} at {session_time.strftime('%Y-%m-%d %H:%M')}"
|
|
await self.notifier.notify_session_booking(session_details)
|
|
else:
|
|
logging.error(f"Failed to book {session_type} session at {session_time}")
|
|
|
|
# Notify about failed booking
|
|
session_details = f"{session['name_activity']} at {session_time.strftime('%Y-%m-%d %H:%M')}"
|
|
await self.notifier.notify_impossible_booking(session_details)
|
|
logging.info(f"Notified about impossible booking for {session_type} session at {session_time}")
|
|
|
|
# Display all booked session(s)
|
|
if booked_sessions:
|
|
logging.info("Booked sessions:")
|
|
for session in booked_sessions:
|
|
session_time: datetime = parse(session["start_timestamp"])
|
|
if not session_time.tzinfo:
|
|
session_time = pytz.timezone(TIMEZONE).localize(session_time)
|
|
logging.info(f"ID: {session['id_activity_calendar']}, Name: {session['name_activity']}, Time: {session_time.strftime('%Y-%m-%d %H:%M')}")
|
|
else:
|
|
logging.info("No sessions were booked")
|
|
|
|
async def run(self) -> None:
|
|
"""
|
|
Main execution loop.
|
|
"""
|
|
# Set up timezone
|
|
tz: pytz.timezone = pytz.timezone(TIMEZONE)
|
|
|
|
# Parse TARGET_RESERVATION_TIME to get the target hour and minute
|
|
target_hour, target_minute = map(int, TARGET_RESERVATION_TIME.split(":"))
|
|
target_time = datetime.now(tz).replace(hour=target_hour, minute=target_minute, second=0, microsecond=0)
|
|
booking_window_end = target_time + timedelta(minutes=BOOKING_WINDOW_END_DELTA_MINUTES)
|
|
|
|
# Initial login
|
|
if not self.auth_handler.login():
|
|
logging.error("Authentication failed - exiting program")
|
|
return
|
|
|
|
try:
|
|
while True:
|
|
try:
|
|
current_time: datetime = datetime.now(tz)
|
|
logging.info(f"Current time: {current_time}")
|
|
|
|
# Only book sessions if current time is within the booking window
|
|
if target_time <= current_time <= booking_window_end:
|
|
# Run booking cycle to check for preferred sessions and book
|
|
await self.booker(current_time)
|
|
# Wait for a short time before next check
|
|
time.sleep(60)
|
|
else:
|
|
# Check again in 5 minutes if outside booking window
|
|
time.sleep(300)
|
|
except Exception as e:
|
|
logging.error(f"Unexpected error in booking cycle: {str(e)} - Traceback: {traceback.format_exc()}")
|
|
time.sleep(60) # Wait before retrying after error
|
|
except KeyboardInterrupt:
|
|
self.quit()
|
|
|
|
def quit(self) -> None:
|
|
"""
|
|
Clean up resources and exit the script.
|
|
"""
|
|
logging.info("Script interrupted by user. Quitting...")
|
|
exit(0)
|
|
|
|
def get_available_sessions(self, start_date: date, end_date: date) -> Optional[Dict[str, Any]]:
|
|
"""
|
|
Fetch available sessions from the API.
|
|
|
|
Args:
|
|
start_date (date): Start date for fetching sessions.
|
|
end_date (date): End date for fetching sessions.
|
|
|
|
Returns:
|
|
Optional[Dict[str, Any]]: Dictionary containing available sessions if successful, None otherwise.
|
|
"""
|
|
from session_manager import SessionManager
|
|
session_manager = SessionManager(self.auth_handler)
|
|
return session_manager.get_available_sessions(start_date, end_date)
|
|
|
|
def book_session(self, session_id: str) -> bool:
|
|
"""
|
|
Book a specific session.
|
|
|
|
Args:
|
|
session_id (str): ID of the session to book.
|
|
|
|
Returns:
|
|
bool: True if booking is successful, False otherwise.
|
|
"""
|
|
from session_manager import SessionManager
|
|
session_manager = SessionManager(self.auth_handler)
|
|
return session_manager.book_session(session_id)
|
|
|
|
def get_booked_sessions(self) -> List[Dict[str, Any]]:
|
|
"""
|
|
Get a list of booked sessions.
|
|
|
|
Returns:
|
|
A list of dictionaries containing information about the booked sessions.
|
|
"""
|
|
from session_manager import SessionManager
|
|
session_manager = SessionManager(self.auth_handler)
|
|
return session_manager.get_booked_sessions()
|
|
|
|
def is_session_bookable(self, session: Dict[str, Any], current_time: datetime) -> bool:
|
|
"""
|
|
Check if a session is bookable based on user_info.
|
|
|
|
Args:
|
|
session (Dict[str, Any]): Session data.
|
|
current_time (datetime): Current time for comparison.
|
|
|
|
Returns:
|
|
bool: True if the session is bookable, False otherwise.
|
|
"""
|
|
from session_manager import SessionManager
|
|
session_manager = SessionManager(self.auth_handler)
|
|
return session_manager.is_session_bookable(session, current_time)
|
|
|
|
def matches_preferred_session(self, session: Dict[str, Any], current_time: datetime) -> bool:
|
|
"""
|
|
Check if session matches one of your preferred sessions with exact matching.
|
|
|
|
Args:
|
|
session (Dict[str, Any]): Session data.
|
|
current_time (datetime): Current time for comparison.
|
|
|
|
Returns:
|
|
bool: True if the session matches a preferred session, False otherwise.
|
|
"""
|
|
from session_manager import SessionManager
|
|
session_manager = SessionManager(self.auth_handler)
|
|
return session_manager.matches_preferred_session(session, current_time)
|
|
|
|
def display_upcoming_sessions(self, sessions: List[Dict[str, Any]], current_time: datetime) -> None:
|
|
"""
|
|
Display upcoming sessions with ID, name, date, and time.
|
|
|
|
Args:
|
|
sessions (List[Dict[str, Any]]): List of session data.
|
|
current_time (datetime): Current time for comparison.
|
|
"""
|
|
from session_manager import SessionManager
|
|
session_manager = SessionManager(self.auth_handler)
|
|
session_manager.display_upcoming_sessions(sessions, current_time) |