337
README.md
Normal file
337
README.md
Normal file
@@ -0,0 +1,337 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user