A lot of features #1

Merged
kbe merged 11 commits from develop into main 2025-07-20 14:32:07 +00:00
3 changed files with 157 additions and 11 deletions
Showing only changes of commit 5dcc2a89ae - Show all commits

92
README.md Normal file
View File

@@ -0,0 +1,92 @@
# Crossfit Application
This is a Python application for managing Crossfit bookings and notifications. The application automates the process of booking Crossfit sessions and sends notifications via email and Telegram when a booking is successful.
## Features
- Automated booking of Crossfit sessions
- Email and Telegram notifications for successful bookings
- Configurable preferred sessions
- Retry logic for booking failures
- Detailed logging
## Prerequisites
- Docker
- Docker Compose
## Setup
1. Create a `.env` file based on `.env.example` and fill in the required credentials.
2. Build and run the application using Docker Compose:
```bash
docker-compose up --build
```
3. The application will run in a Docker container, and the logs will be stored in the `./log` directory.
## Usage
The application will automatically check for available sessions and book them based on your preferences. It will send notifications via email and Telegram when a booking is successful.
### Environment Variables
The following environment variables are required:
- `CROSSFIT_USERNAME`: Your Crossfit username
- `CROSSFIT_PASSWORD`: Your Crossfit password
- `EMAIL_FROM`: Your email address
- `EMAIL_TO`: Recipient email address
- `EMAIL_PASSWORD`: Your email password
- `TELEGRAM_TOKEN`: Your Telegram bot token
- `TELEGRAM_CHAT_ID`: Your Telegram chat ID
### Preferred Sessions
You can configure your preferred sessions in the `crossfit_booker.py` file. The preferred sessions are defined as a list of tuples, where each tuple contains the day of the week, start time, and session name.
```python
PREFERRED_SESSIONS = [
(4, "17:00", "WEIGHTLIFTING"), # Friday 17:00 WEIGHTLIFTING
(5, "12:30", "HYROX"), # Saturday 12:30 HYROX
(2, "18:30", "CONDITIONING"), # Wednesday 18:30 CONDITIONING
]
```
## Files
- `Dockerfile`: Docker image definition
- `docker-compose.yml`: Docker Compose service definition
- `.env.example`: Example environment variables file
- `.dockerignore`: Docker ignore file
- `.gitignore`: Git ignore file
- `book_crossfit.py`: Main application script
- `crossfit_booker.py`: Crossfit booking script
- `session_notifier.py`: Session notification script
- `requirements.txt`: Python dependencies
## Project Structure
```
.
├── Dockerfile
├── docker-compose.yml
├── .env.example
├── .dockerignore
├── .gitignore
├── book_crossfit.py
├── crossfit_booker.py
├── session_notifier.py
├── requirements.txt
└── log
└── crossfit_booking.log
```
## Contributing
Contributions are welcome! Please open an issue or submit a pull request.
## License
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.

View File

@@ -30,8 +30,7 @@ if not all([USERNAME, PASSWORD]):
APPLICATION_ID = "81560887" APPLICATION_ID = "81560887"
CATEGORY_ID = "677" # Activity category ID for CrossFit CATEGORY_ID = "677" # Activity category ID for CrossFit
TIMEZONE = "Europe/Paris" # Adjust to your timezone TIMEZONE = "Europe/Paris" # Adjust to your timezone
# TARGET_RESERVATION_TIME = "20:01" # When bookings open (8 PM) TARGET_RESERVATION_TIME = "20:01" # When bookings open (8 PM)
TARGET_RESERVATION_TIME = "21:40" # When bookings open (8 PM)
DEVICE_TYPE = "3" DEVICE_TYPE = "3"
# Retry configuration # Retry configuration
@@ -43,6 +42,7 @@ APP_VERSION = "5.09.21"
# Format: List of tuples (day_of_week, start_time, session_name_contains) # Format: List of tuples (day_of_week, start_time, session_name_contains)
# day_of_week: 0=Monday, 6=Sunday # day_of_week: 0=Monday, 6=Sunday
PREFERRED_SESSIONS = [ PREFERRED_SESSIONS = [
# (0, "17:00", "HYROX"), # Monday 17:00 HYROX
(4, "17:00", "WEIGHTLIFTING"), # Friday 17:00 WEIGHTLIFTING (4, "17:00", "WEIGHTLIFTING"), # Friday 17:00 WEIGHTLIFTING
(5, "12:30", "HYROX"), # Saturday 12:30 HYROX (5, "12:30", "HYROX"), # Saturday 12:30 HYROX
(2, "18:30", "CONDITIONING"), # Wednesday 18:30 CONDITIONING (2, "18:30", "CONDITIONING"), # Wednesday 18:30 CONDITIONING
@@ -373,7 +373,8 @@ class CrossFitBooker:
# First check if can_join is true (primary condition) # First check if can_join is true (primary condition)
if user_info.get("can_join", False): if user_info.get("can_join", False):
logging.debug("Session is bookable: can_join is True") activity_name = session.get("name_activity")
logging.debug(f"Session is bookable: {activity_name} can_join is True")
return True return True
# If can_join is False, check if there's a booking window # If can_join is False, check if there's a booking window
@@ -464,19 +465,51 @@ class CrossFitBooker:
activities: List[Dict[str, Any]] = sessions_data.get("data", {}).get("activities_calendar", []) activities: List[Dict[str, Any]] = sessions_data.get("data", {}).get("activities_calendar", [])
# Find sessions to book (prefered only) # Find sessions to book (preferred only)
sessions_to_book: List[Tuple[str, Dict[str, Any]]] = [] sessions_to_book: List[Tuple[str, Dict[str, Any]]] = []
upcoming_sessions: List[Dict[str, Any]] = []
found_preferred_sessions: List[Dict[str, Any]] = []
for session in activities: for session in activities:
if not self.is_session_bookable(session, current_time): session_time: datetime = parse(session["start_timestamp"])
continue if not session_time.tzinfo:
session_time = pytz.timezone(TIMEZONE).localize(session_time)
if self.matches_preferred_session(session, current_time): # Check if session is preferred and bookable
sessions_to_book.append(("Preferred", session)) if self.is_session_bookable(session, current_time):
if self.matches_preferred_session(session, current_time):
sessions_to_book.append(("Preferred", session))
found_preferred_sessions.append(session)
else:
# Check if it's a preferred session that's not bookable yet
if self.matches_preferred_session(session, current_time):
found_preferred_sessions.append(session)
# Check if it's available tomorrow
if (session_time.date() - current_time.date()).days == 1:
upcoming_sessions.append(session)
if not sessions_to_book: if not sessions_to_book and not upcoming_sessions:
logging.info("No matching sessions found to book") logging.info("No matching sessions found to book")
return return
# Notify about all found preferred sessions, regardless of bookability
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)
session_details = f"{session['name_activity']} at {session_time.strftime('%Y-%m-%d %H:%M')}"
self.notifier.notify_session_booking(session_details)
logging.info(f"Notified about found preferred session: {session_details}")
# Notify about upcoming sessions
for session in upcoming_sessions:
session_time: datetime = parse(session["start_timestamp"])
if not session_time.tzinfo:
session_time = pytz.timezone(TIMEZONE).localize(session_time)
session_details = f"{session['name_activity']} at {session_time.strftime('%Y-%m-%d %H:%M')}"
self.notifier.notify_upcoming_session(session_details, 1) # Days until is 1 for tomorrow
logging.info(f"Notified about upcoming session: {session_details}")
# Book sessions (preferred first) # Book sessions (preferred first)
sessions_to_book.sort(key=lambda x: 0 if x[0] == "Preferred" else 1) sessions_to_book.sort(key=lambda x: 0 if x[0] == "Preferred" else 1)
for session_type, session in sessions_to_book: for session_type, session in sessions_to_book:
@@ -508,9 +541,11 @@ class CrossFitBooker:
current_time: datetime = datetime.now(tz) current_time: datetime = datetime.now(tz)
logging.info(f"Current time: {current_time}") logging.info(f"Current time: {current_time}")
# Run booking cycle at the target time or if it's a test, with optimized checking # Always run booking cycle to check for preferred sessions and notify
self.run_booking_cycle(current_time)
# Run booking cycle at the target time for actual booking
if current_time.strftime("%H:%M") == TARGET_RESERVATION_TIME: if current_time.strftime("%H:%M") == TARGET_RESERVATION_TIME:
self.run_booking_cycle(current_time)
# Wait until the next booking window # Wait until the next booking window
wait_until = current_time + timedelta(minutes=60) wait_until = current_time + timedelta(minutes=60)
time.sleep((wait_until - current_time).total_seconds()) time.sleep((wait_until - current_time).total_seconds())

View File

@@ -95,6 +95,25 @@ class SessionNotifier:
email_message = f"Session booked: {session_details}" email_message = f"Session booked: {session_details}"
telegram_message = f"Session booked: {session_details}" telegram_message = f"Session booked: {session_details}"
# Send notifications through enabled channels
if self.enable_email:
self.send_email_notification(email_message)
if self.enable_telegram:
self.send_telegram_notification(telegram_message)
def notify_upcoming_session(self, session_details, days_until):
"""
Notify about an upcoming session via email and Telegram.
Args:
session_details (str): Details about the upcoming session
days_until (int): Number of days until the session
"""
# Create messages for both email and Telegram
email_message = f"Session available soon: {session_details} (in {days_until} days)"
telegram_message = f"Session available soon: {session_details} (in {days_until} days)"
# Send notifications through enabled channels # Send notifications through enabled channels
if self.enable_email: if self.enable_email:
self.send_email_notification(email_message) self.send_email_notification(email_message)