Initial commit

Signed-off-by: Thomas Klaehn <thomas.klaehn@perinet.io>
This commit is contained in:
Thomas Klaehn
2026-02-10 11:47:49 +01:00
commit f496eebe2a
17 changed files with 1835 additions and 0 deletions

337
README.md Normal file
View 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