338 lines
8.7 KiB
Markdown
338 lines
8.7 KiB
Markdown
# Zwift Activity Monitor
|
|
|
|
Automatic monitoring service that continuously watches for new Zwift activities and downloads their FIT files.
|
|
|
|
## Features
|
|
|
|
- ✅ **Continuous Monitoring**: Automatically checks for new activities every 5 minutes (configurable)
|
|
- ✅ **Automatic Downloads**: Downloads FIT files for new activities as soon as they're available
|
|
- ✅ **Docker Support**: Easy deployment with Docker and Docker Compose
|
|
- ✅ **Persistent State**: Tracks downloaded activities to prevent duplicates
|
|
- ✅ **Rate Limiting**: Respects API rate limits with built-in throttling
|
|
- ✅ **Graceful Shutdown**: Handles SIGTERM/SIGINT for clean container restarts
|
|
- ✅ **Structured Logging**: JSON logging with configurable levels
|
|
- ✅ **Retry Logic**: Automatic retry with exponential backoff for transient failures
|
|
|
|
## Quick Start
|
|
|
|
### Using Docker Compose (Recommended)
|
|
|
|
1. **Clone the repository**
|
|
```bash
|
|
git clone <your-repo-url>
|
|
cd zwift
|
|
```
|
|
|
|
2. **Create environment file**
|
|
```bash
|
|
cat > .env <<EOF
|
|
ZWIFT_USERNAME=your@email.com
|
|
ZWIFT_PASSWORD=yourpassword
|
|
TZ=Europe/Berlin
|
|
EOF
|
|
```
|
|
|
|
3. **Create data directory**
|
|
```bash
|
|
mkdir -p data/activities
|
|
```
|
|
|
|
4. **Start the service**
|
|
```bash
|
|
docker-compose up -d
|
|
```
|
|
|
|
5. **View logs**
|
|
```bash
|
|
docker-compose logs -f
|
|
```
|
|
|
|
6. **Check downloaded files**
|
|
```bash
|
|
ls -lh data/activities/
|
|
```
|
|
|
|
### Using Docker
|
|
|
|
```bash
|
|
docker build -t zwift-monitor .
|
|
|
|
docker run -d \
|
|
--name zwift-monitor \
|
|
--restart unless-stopped \
|
|
-e ZWIFT_USERNAME=your@email.com \
|
|
-e ZWIFT_PASSWORD=yourpassword \
|
|
-e TZ=Europe/Berlin \
|
|
-v $(pwd)/data:/data \
|
|
zwift-monitor
|
|
```
|
|
|
|
### Local Development
|
|
|
|
```bash
|
|
# Install dependencies
|
|
go mod download
|
|
|
|
# Set credentials
|
|
export ZWIFT_USERNAME=your@email.com
|
|
export ZWIFT_PASSWORD=yourpassword
|
|
export ZWIFT_OUTPUT_DIR=./data/activities
|
|
export ZWIFT_STATE_FILE=./data/state.json
|
|
|
|
# Run the service
|
|
go run .
|
|
```
|
|
|
|
## Configuration
|
|
|
|
### Environment Variables
|
|
|
|
| Variable | Default | Description |
|
|
|----------|---------|-------------|
|
|
| `ZWIFT_USERNAME` | *required* | Your Zwift email address |
|
|
| `ZWIFT_PASSWORD` | *required* | Your Zwift password |
|
|
| `ZWIFT_OUTPUT_DIR` | `/data/activities` | Directory for FIT files |
|
|
| `ZWIFT_STATE_FILE` | `/data/zwift-state.json` | State persistence file |
|
|
| `ZWIFT_TOKEN_CACHE` | `/data/.zwift-token.json` | OAuth token cache |
|
|
| `ZWIFT_POLL_INTERVAL` | `5m` | How often to check for new activities |
|
|
| `ZWIFT_RATE_LIMIT` | `5` | Maximum requests per second |
|
|
| `ZWIFT_MAX_RETRIES` | `3` | Number of retries for failed requests |
|
|
| `ZWIFT_LOG_LEVEL` | `info` | Logging level (debug, info, warn, error) |
|
|
| `ZWIFT_LOG_FILE` | `` | Optional log file path |
|
|
| `TZ` | `UTC` | Timezone for timestamps |
|
|
|
|
### Configuration File (Optional)
|
|
|
|
You can also use a `config.yaml` file:
|
|
|
|
```yaml
|
|
username: your@email.com
|
|
password: yourpassword
|
|
output_dir: /data/activities
|
|
poll_interval: 5m
|
|
rate_limit: 5
|
|
max_retries: 3
|
|
log_level: info
|
|
```
|
|
|
|
Place the config file:
|
|
- Local: `config.yaml` in the project directory
|
|
- Docker: Mount as `/app/config.yaml`
|
|
|
|
**Note**: Environment variables take precedence over config file values.
|
|
|
|
## File Naming
|
|
|
|
Downloaded FIT files are named using the format:
|
|
```
|
|
{activityID}_{date}_{activity-name}.fit
|
|
```
|
|
|
|
Example:
|
|
```
|
|
12345678_2025-02-10_Morning-Ride.fit
|
|
```
|
|
|
|
## Logging
|
|
|
|
The service uses structured logging with the following levels:
|
|
|
|
- **DEBUG**: Detailed API calls and responses
|
|
- **INFO**: Normal operation, downloads, statistics
|
|
- **WARN**: Retries, non-fatal errors
|
|
- **ERROR**: Failed downloads, authentication issues
|
|
|
|
### Example Log Output
|
|
|
|
```
|
|
2025-02-10T10:00:00Z INFO Starting Zwift Activity Monitor poll_interval=5m0s output_dir=/data/activities
|
|
2025-02-10T10:00:01Z INFO Loaded state total_downloaded=42 last_check=2025-02-10T09:55:00Z
|
|
2025-02-10T10:00:02Z INFO Running initial check...
|
|
2025-02-10T10:00:03Z INFO Checking for new activities...
|
|
2025-02-10T10:00:05Z INFO Found new activities count=1
|
|
2025-02-10T10:00:06Z INFO Downloading activity activity_id=12345678 name="Morning Ride"
|
|
2025-02-10T10:00:08Z INFO Successfully downloaded activity activity_id=12345678 filepath=/data/activities/12345678_2025-02-10_Morning-Ride.fit
|
|
2025-02-10T10:00:08Z INFO Download complete downloaded=1 total_new=1
|
|
```
|
|
|
|
## State Management
|
|
|
|
The service maintains a JSON state file (`zwift-state.json`) that tracks:
|
|
- All downloaded activity IDs
|
|
- Activity metadata (name, date, file path)
|
|
- Last check timestamp
|
|
- Total downloads
|
|
|
|
### Example State File
|
|
|
|
```json
|
|
{
|
|
"downloaded_activities": {
|
|
"12345678": {
|
|
"id": 12345678,
|
|
"name": "Morning Ride",
|
|
"date": "2025-02-10T08:30:00Z",
|
|
"file_path": "/data/activities/12345678_2025-02-10_Morning-Ride.fit",
|
|
"downloaded_at": "2025-02-10T10:00:08Z",
|
|
"sport_type": "cycling"
|
|
}
|
|
},
|
|
"last_check": "2025-02-10T10:00:08Z",
|
|
"total_downloaded": 1
|
|
}
|
|
```
|
|
|
|
This state file ensures:
|
|
- No duplicate downloads after restarts
|
|
- Resume capability after crashes
|
|
- Historical tracking of all downloads
|
|
|
|
## Troubleshooting
|
|
|
|
### Authentication Fails
|
|
|
|
```
|
|
ERROR Failed to create service error="failed to create Zwift client: ..."
|
|
```
|
|
|
|
**Solution**: Verify your Zwift credentials are correct.
|
|
|
|
### No New Activities Detected
|
|
|
|
```
|
|
INFO No new activities found
|
|
```
|
|
|
|
**Possible causes**:
|
|
- No new activities since last check
|
|
- Activities already downloaded
|
|
- Activity privacy settings
|
|
|
|
**Check**: View your state file to see what's already downloaded:
|
|
```bash
|
|
cat data/zwift-state.json | jq .
|
|
```
|
|
|
|
### Rate Limiting Issues
|
|
|
|
```
|
|
WARN Rate limited by Zwift API
|
|
```
|
|
|
|
**Solution**: Increase the poll interval:
|
|
```bash
|
|
ZWIFT_POLL_INTERVAL=10m # Check every 10 minutes instead of 5
|
|
```
|
|
|
|
### Disk Space Issues
|
|
|
|
```
|
|
ERROR Failed to save FIT file error="no space left on device"
|
|
```
|
|
|
|
**Solution**: Free up disk space or mount a larger volume.
|
|
|
|
## Docker Commands
|
|
|
|
```bash
|
|
# View logs
|
|
docker-compose logs -f
|
|
|
|
# Restart service
|
|
docker-compose restart
|
|
|
|
# Stop service
|
|
docker-compose down
|
|
|
|
# Rebuild after code changes
|
|
docker-compose up -d --build
|
|
|
|
# View container stats
|
|
docker stats zwift-monitor
|
|
|
|
# Access container shell
|
|
docker-compose exec zwift-monitor sh
|
|
|
|
# Check state file
|
|
docker-compose exec zwift-monitor cat /data/zwift-state.json
|
|
```
|
|
|
|
## Building from Source
|
|
|
|
```bash
|
|
# Build binary
|
|
go build -o zwift-monitor .
|
|
|
|
# Run tests (when available)
|
|
go test ./...
|
|
|
|
# Build Docker image
|
|
docker build -t zwift-monitor .
|
|
|
|
# Cross-compile for different platforms
|
|
GOOS=linux GOARCH=amd64 go build -o zwift-monitor-linux-amd64
|
|
GOOS=darwin GOARCH=arm64 go build -o zwift-monitor-darwin-arm64
|
|
```
|
|
|
|
## Architecture
|
|
|
|
```
|
|
zwift/
|
|
├── main.go # Entry point with signal handling
|
|
├── internal/
|
|
│ ├── service/ # Main service loop
|
|
│ │ └── service.go # Ticker-based monitoring
|
|
│ ├── monitor/ # Activity monitoring logic
|
|
│ │ └── monitor.go # Check and download orchestration
|
|
│ ├── client/ # Zwift API client
|
|
│ │ ├── client.go # API wrapper with rate limiting
|
|
│ │ └── auth.go # OAuth token management
|
|
│ ├── state/ # State persistence
|
|
│ │ └── state.go # Thread-safe JSON state
|
|
│ ├── storage/ # File system operations
|
|
│ │ └── storage.go # Atomic FIT file writes
|
|
│ └── config/ # Configuration management
|
|
│ └── config.go # Env vars + YAML loading
|
|
└── Dockerfile # Multi-stage Docker build
|
|
```
|
|
|
|
## Security Considerations
|
|
|
|
- **Credentials**: Never commit credentials to git. Use environment variables or Docker secrets.
|
|
- **Token Cache**: Stored with 0600 permissions (owner read/write only).
|
|
- **State File**: Contains activity IDs but no sensitive data.
|
|
- **Container**: Runs as non-root user for security.
|
|
|
|
## API Usage
|
|
|
|
This service uses the unofficial Zwift API via the [gravl](https://github.com/bzimmer/gravl) library. The API:
|
|
|
|
- Is not officially supported by Zwift
|
|
- May change without notice
|
|
- Requires regular Zwift credentials (no special API key needed)
|
|
- Respects rate limiting (5 requests/second by default)
|
|
|
|
## License
|
|
|
|
[Your License Here]
|
|
|
|
## Contributing
|
|
|
|
Contributions welcome! Please:
|
|
1. Fork the repository
|
|
2. Create a feature branch
|
|
3. Make your changes
|
|
4. Submit a pull request
|
|
|
|
## Support
|
|
|
|
For issues or questions:
|
|
- Create an issue in the repository
|
|
- Check existing issues for similar problems
|
|
- Include logs and configuration (redact credentials!)
|
|
|
|
## Acknowledgments
|
|
|
|
- [gravl](https://github.com/bzimmer/gravl) - Go library for Zwift API
|
|
- [Zwift](https://zwift.com) - Virtual cycling platform
|