feat: Add notification for upcoming sessions
This commit is contained in:
92
README.md
Normal file
92
README.md
Normal 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.
|
||||||
@@ -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())
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user