mirror of
https://github.com/sstent/Garmin_Analyser.git
synced 2025-12-05 23:52:11 +00:00
trying to fix activity dl
This commit is contained in:
101
README.md
101
README.md
@@ -37,84 +37,105 @@ pip install weasyprint
|
||||
|
||||
### Basic Usage
|
||||
|
||||
The application uses a subcommand-based CLI structure. Here are some basic examples:
|
||||
|
||||
Analyze a single workout file:
|
||||
```bash
|
||||
python main.py --file path/to/workout.fit --report --charts
|
||||
python main.py analyze --file path/to/workout.fit --report --charts
|
||||
```
|
||||
|
||||
Analyze all workouts in a directory:
|
||||
```bash
|
||||
python main.py --directory path/to/workouts --summary --format html
|
||||
python main.py batch --directory path/to/workouts --summary --format html
|
||||
```
|
||||
|
||||
Download from Garmin Connect:
|
||||
Download and analyze latest workout from Garmin Connect:
|
||||
```bash
|
||||
python main.py --garmin-connect --report --charts --summary
|
||||
python main.py analyze --garmin-connect --report --charts
|
||||
```
|
||||
|
||||
Download all cycling activities from Garmin Connect:
|
||||
```bash
|
||||
python main.py download --all --limit 100 --output-dir data/garmin_downloads
|
||||
```
|
||||
|
||||
Re-analyze previously downloaded workouts:
|
||||
```bash
|
||||
python main.py reanalyze --input-dir data/garmin_downloads --output-dir reports/reanalysis --charts --report
|
||||
```
|
||||
|
||||
Show current configuration:
|
||||
```bash
|
||||
python main.py config --show
|
||||
```
|
||||
|
||||
### Command Line Options
|
||||
|
||||
For a full list of commands and options, run:
|
||||
```bash
|
||||
python main.py --help
|
||||
python main.py [command] --help
|
||||
```
|
||||
usage: main.py [-h] [--config CONFIG] [--verbose]
|
||||
(--file FILE | --directory DIRECTORY | --garmin-connect | --workout-id WORKOUT_ID | --download-all | --reanalyze-all)
|
||||
[--ftp FTP] [--max-hr MAX_HR] [--zones ZONES] [--cog COG]
|
||||
[--output-dir OUTPUT_DIR] [--format {html,pdf,markdown}]
|
||||
[--charts] [--report] [--summary]
|
||||
|
||||
Example output for `python main.py --help`:
|
||||
```
|
||||
usage: main.py [-h] [--verbose] {analyze,batch,download,reanalyze,config} ...
|
||||
|
||||
Analyze Garmin workout data from files or Garmin Connect
|
||||
|
||||
positional arguments:
|
||||
{analyze,batch,download,reanalyze,config}
|
||||
Available commands
|
||||
analyze Analyze a single workout or download from Garmin Connect
|
||||
batch Analyze multiple workout files in a directory
|
||||
download Download activities from Garmin Connect
|
||||
reanalyze Re-analyze all downloaded activities
|
||||
config Manage configuration
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--config CONFIG, -c CONFIG
|
||||
Configuration file path
|
||||
--verbose, -v Enable verbose logging
|
||||
```
|
||||
|
||||
Input options:
|
||||
Example output for `python main.py analyze --help`:
|
||||
```
|
||||
usage: main.py analyze [-h] [--file FILE] [--garmin-connect] [--workout-id WORKOUT_ID]
|
||||
[--ftp FTP] [--max-hr MAX_HR] [--zones ZONES] [--cog COG]
|
||||
[--output-dir OUTPUT_DIR] [--format {html,pdf,markdown}]
|
||||
[--charts] [--report]
|
||||
|
||||
Analyze a single workout or download from Garmin Connect
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--file FILE, -f FILE Path to workout file (FIT, TCX, or GPX)
|
||||
--directory DIRECTORY, -d DIRECTORY
|
||||
Directory containing workout files
|
||||
--garmin-connect Download from Garmin Connect
|
||||
--garmin-connect Download and analyze latest workout from Garmin Connect
|
||||
--workout-id WORKOUT_ID
|
||||
Analyze specific workout by ID from Garmin Connect
|
||||
--download-all Download all cycling activities from Garmin Connect (no analysis)
|
||||
--reanalyze-all Re-analyze all downloaded activities and generate reports
|
||||
|
||||
Analysis options:
|
||||
--ftp FTP Functional Threshold Power (W)
|
||||
--max-hr MAX_HR Maximum heart rate (bpm)
|
||||
--zones ZONES Path to zones configuration file
|
||||
--cog COG Cog size (teeth) for power calculations. Auto-detected if not provided
|
||||
|
||||
Output options:
|
||||
--output-dir OUTPUT_DIR
|
||||
Output directory for reports and charts
|
||||
--format {html,pdf,markdown}
|
||||
Report format
|
||||
--charts Generate charts
|
||||
--report Generate comprehensive report
|
||||
--summary Generate summary report for multiple workouts
|
||||
|
||||
Examples:
|
||||
Analyze latest workout from Garmin Connect: python main.py --garmin-connect
|
||||
Analyze specific workout by ID: python main.py --workout-id 123456789
|
||||
Download all cycling workouts: python main.py --download-all
|
||||
Re-analyze all downloaded workouts: python main.py --reanalyze-all
|
||||
Analyze local FIT file: python main.py --file path/to/workout.fit
|
||||
Analyze directory of workouts: python main.py --directory data/
|
||||
|
||||
Configuration:
|
||||
Set Garmin credentials in .env file: GARMIN_EMAIL and GARMIN_PASSWORD
|
||||
Configure zones in config/config.yaml or use --zones flag
|
||||
Override FTP with --ftp flag, max HR with --max-hr flag
|
||||
|
||||
Output:
|
||||
Reports saved to output/ directory by default
|
||||
Charts saved to output/charts/ when --charts is used
|
||||
```
|
||||
|
||||
### Configuration:
|
||||
Set Garmin credentials in `.env` file: `GARMIN_EMAIL` and `GARMIN_PASSWORD`.
|
||||
Configure zones in `config/config.yaml` or use `--zones` flag.
|
||||
Override FTP with `--ftp` flag, max HR with `--max-hr` flag.
|
||||
|
||||
### Output:
|
||||
Reports saved to `output/` directory by default.
|
||||
Charts saved to `output/charts/` when `--charts` is used.
|
||||
|
||||
## Deprecation Notice
|
||||
|
||||
The Text User Interface (TUI) and legacy analyzer have been removed in favor of the more robust and maintainable modular command-line interface (CLI). The project now relies exclusively on the modular CLI (`main.py` and `cli.py`) for all operations. All functionality from the legacy components has been successfully migrated to the modular stack.
|
||||
The Text User Interface (TUI) and legacy analyzer have been removed in favor of the more robust and maintainable modular command-line interface (CLI) implemented solely in `main.py`. The `cli.py` file has been removed. All functionality from the legacy components has been successfully migrated to the modular stack.
|
||||
|
||||
## Setup credentials
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ class GarminClient:
|
||||
|
||||
Args:
|
||||
activity_id: Garmin activity ID
|
||||
file_format: File format to download (fit, tcx, gpx)
|
||||
file_format: File format to download (fit, tcx, gpx, csv, original)
|
||||
|
||||
Returns:
|
||||
Path to downloaded file or None if download failed
|
||||
@@ -134,22 +134,31 @@ class GarminClient:
|
||||
try:
|
||||
# Create data directory if it doesn't exist
|
||||
DATA_DIR.mkdir(exist_ok=True)
|
||||
|
||||
fmt_upper = (file_format or "").upper()
|
||||
logger.debug(f"download_activity_file: requested format='{file_format}' normalized='{fmt_upper}'")
|
||||
|
||||
# Download file
|
||||
file_data = self.client.download_activity(
|
||||
activity_id,
|
||||
dl_fmt=file_format.upper()
|
||||
)
|
||||
|
||||
# Save to file
|
||||
filename = f"activity_{activity_id}.{file_format}"
|
||||
file_path = DATA_DIR / filename
|
||||
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(file_data)
|
||||
|
||||
logger.info(f"Downloaded activity file: {file_path}")
|
||||
return file_path
|
||||
if fmt_upper in {"TCX", "GPX", "CSV"}:
|
||||
# Direct format downloads supported by garminconnect
|
||||
file_data = self.client.download_activity(activity_id, dl_fmt=fmt_upper)
|
||||
|
||||
# Save to file using lowercase extension
|
||||
filename = f"activity_{activity_id}.{fmt_upper.lower()}"
|
||||
file_path = DATA_DIR / filename
|
||||
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(file_data)
|
||||
|
||||
logger.info(f"Downloaded activity file: {file_path}")
|
||||
return file_path
|
||||
|
||||
# FIT is not a direct dl_fmt in some client versions; use ORIGINAL to obtain ZIP and extract .fit
|
||||
if fmt_upper in {"FIT", "ORIGINAL"} or file_format.lower() == "fit":
|
||||
fit_path = self.download_activity_original(activity_id)
|
||||
return fit_path
|
||||
|
||||
logger.error(f"Unsupported download format '{file_format}'. Valid: GPX, TCX, ORIGINAL, CSV")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to download activity {activity_id}: {e}")
|
||||
@@ -172,39 +181,196 @@ class GarminClient:
|
||||
# Create data directory if it doesn't exist
|
||||
DATA_DIR.mkdir(exist_ok=True)
|
||||
|
||||
# Download original file
|
||||
file_data = self.client.download_activity(activity_id, dl_fmt='ZIP')
|
||||
# Capability probe: does garminconnect client expose a native original download?
|
||||
has_native_original = hasattr(self.client, 'download_activity_original')
|
||||
logger.debug(f"garminconnect has download_activity_original: {has_native_original}")
|
||||
|
||||
file_data = None
|
||||
attempts: List[str] = []
|
||||
|
||||
# 1) Prefer native method when available
|
||||
if has_native_original:
|
||||
try:
|
||||
attempts.append("self.client.download_activity_original(activity_id)")
|
||||
logger.debug(f"Attempting native download_activity_original for activity {activity_id}")
|
||||
file_data = self.client.download_original_activity(activity_id)
|
||||
except Exception as e:
|
||||
logger.debug(f"Native download_activity_original failed: {e} (type={type(e).__name__})")
|
||||
file_data = None
|
||||
|
||||
# 2) Try download_activity with 'original' format
|
||||
if file_data is None and hasattr(self.client, 'download_activity'):
|
||||
try:
|
||||
attempts.append("self.client.download_activity(activity_id, dl_fmt='original')")
|
||||
logger.debug(f"Attempting original download via download_activity(dl_fmt='original') for activity {activity_id}")
|
||||
file_data = self.client.download_activity(activity_id, dl_fmt='original')
|
||||
logger.debug(f"download_activity(dl_fmt='original') succeeded, got data type: {type(file_data).__name__}, length: {len(file_data) if hasattr(file_data, '__len__') else 'N/A'}")
|
||||
if file_data is not None and hasattr(file_data, '__len__') and len(file_data) > 0:
|
||||
logger.debug(f"First 100 bytes: {file_data[:100]}")
|
||||
except Exception as e:
|
||||
logger.debug(f"download_activity(dl_fmt='original') failed: {e} (type={type(e).__name__})")
|
||||
file_data = None
|
||||
|
||||
# 3) Try download_activity with positional token (older signatures)
|
||||
if file_data is None and hasattr(self.client, 'download_activity'):
|
||||
tokens_to_try_pos = ['ORIGINAL', 'original', 'FIT', 'fit']
|
||||
for token in tokens_to_try_pos:
|
||||
try:
|
||||
attempts.append(f"self.client.download_activity(activity_id, '{token}')")
|
||||
logger.debug(f"Attempting original download via download_activity(activity_id, '{token}') for activity {activity_id}")
|
||||
file_data = self.client.download_activity(activity_id, token)
|
||||
logger.debug(f"download_activity(activity_id, '{token}') succeeded, got data type: {type(file_data).__name__}, length: {len(file_data) if hasattr(file_data, '__len__') else 'N/A'}")
|
||||
if file_data is not None and hasattr(file_data, '__len__') and len(file_data) > 0:
|
||||
logger.debug(f"First 100 bytes: {file_data[:100]}")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"download_activity(activity_id, '{token}') failed: {e} (type={type(e).__name__})")
|
||||
file_data = None
|
||||
|
||||
# 4) Try alternate method names commonly seen in different garminconnect variants
|
||||
alt_methods_with_format = [
|
||||
('download_activity_file', ['ORIGINAL', 'original', 'FIT', 'fit']),
|
||||
]
|
||||
alt_methods_no_format = [
|
||||
'download_original_activity',
|
||||
'get_original_activity',
|
||||
]
|
||||
|
||||
if file_data is None:
|
||||
for method_name, fmts in alt_methods_with_format:
|
||||
if hasattr(self.client, method_name):
|
||||
method = getattr(self.client, method_name)
|
||||
for fmt in fmts:
|
||||
try:
|
||||
attempts.append(f"self.client.{method_name}(activity_id, '{fmt}')")
|
||||
logger.debug(f"Attempting {method_name}(activity_id, '{fmt}') for activity {activity_id}")
|
||||
file_data = method(activity_id, fmt)
|
||||
logger.debug(f"{method_name}(activity_id, '{fmt}') succeeded, got data type: {type(file_data).__name__}")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"{method_name}(activity_id, '{fmt}') failed: {e} (type={type(e).__name__})")
|
||||
file_data = None
|
||||
if file_data is not None:
|
||||
break
|
||||
|
||||
if file_data is None:
|
||||
for method_name in alt_methods_no_format:
|
||||
if hasattr(self.client, method_name):
|
||||
method = getattr(self.client, method_name)
|
||||
try:
|
||||
attempts.append(f"self.client.{method_name}(activity_id)")
|
||||
logger.debug(f"Attempting {method_name}(activity_id) for activity {activity_id}")
|
||||
file_data = method(activity_id)
|
||||
logger.debug(f"{method_name}(activity_id) succeeded, got data type: {type(file_data).__name__}")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"{method_name}(activity_id) failed: {e} (type={type(e).__name__})")
|
||||
file_data = None
|
||||
|
||||
if file_data is None:
|
||||
# 5) HTTP fallback using authenticated requests session from garminconnect client
|
||||
session = None
|
||||
# Try common attributes that hold a requests.Session or similar
|
||||
for attr in ("session", "_session", "requests_session", "req_session", "http", "client"):
|
||||
candidate = getattr(self.client, attr, None)
|
||||
if candidate is not None and hasattr(candidate, "get"):
|
||||
session = candidate
|
||||
break
|
||||
if candidate is not None and hasattr(candidate, "session") and hasattr(candidate.session, "get"):
|
||||
session = candidate.session
|
||||
break
|
||||
|
||||
if session is not None:
|
||||
http_urls = [
|
||||
f"https://connect.garmin.com/modern/proxy/download-service/export/original/{activity_id}",
|
||||
f"https://connect.garmin.com/modern/proxy/download-service/files/activity/{activity_id}",
|
||||
f"https://connect.garmin.com/modern/proxy/download-service/export/zip/activity/{activity_id}",
|
||||
]
|
||||
for url in http_urls:
|
||||
try:
|
||||
attempts.append(f"HTTP GET {url}")
|
||||
logger.debug(f"Attempting HTTP fallback GET for original: {url}")
|
||||
resp = session.get(url, timeout=30)
|
||||
status = getattr(resp, "status_code", None)
|
||||
content = getattr(resp, "content", None)
|
||||
if status == 200 and content:
|
||||
content_type = getattr(resp, "headers", {}).get("Content-Type", "")
|
||||
logger.debug(f"HTTP fallback succeeded: status={status}, content-type='{content_type}', bytes={len(content)}")
|
||||
file_data = content
|
||||
break
|
||||
else:
|
||||
logger.debug(f"HTTP fallback GET {url} returned status={status} or empty content")
|
||||
except Exception as e:
|
||||
logger.debug(f"HTTP fallback GET {url} failed: {e} (type={type(e).__name__})")
|
||||
|
||||
if file_data is None:
|
||||
logger.error(
|
||||
f"Failed to obtain original/FIT data for activity {activity_id}. "
|
||||
f"Attempts: {attempts}"
|
||||
)
|
||||
return None
|
||||
|
||||
# Normalize to raw bytes if response-like object returned
|
||||
if hasattr(file_data, 'content'):
|
||||
try:
|
||||
file_data = file_data.content
|
||||
except Exception:
|
||||
pass
|
||||
elif hasattr(file_data, 'read'):
|
||||
try:
|
||||
file_data = file_data.read()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not isinstance(file_data, (bytes, bytearray)):
|
||||
logger.error(f"Downloaded data for activity {activity_id} is not bytes (type={type(file_data).__name__}); aborting")
|
||||
logger.debug(f"Data content: {repr(file_data)[:200]}")
|
||||
return None
|
||||
|
||||
# Save to temporary file first
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as tmp_file:
|
||||
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
|
||||
tmp_file.write(file_data)
|
||||
tmp_path = Path(tmp_file.name)
|
||||
|
||||
# Extract zip file
|
||||
with zipfile.ZipFile(tmp_path, 'r') as zip_ref:
|
||||
# Find the first FIT file in the zip
|
||||
fit_files = [f for f in zip_ref.namelist() if f.lower().endswith('.fit')]
|
||||
|
||||
if fit_files:
|
||||
# Extract the first FIT file
|
||||
fit_filename = fit_files[0]
|
||||
extracted_path = DATA_DIR / f"activity_{activity_id}.fit"
|
||||
# Determine if the response is a ZIP archive (original) or a direct FIT file
|
||||
if zipfile.is_zipfile(tmp_path):
|
||||
# Extract zip file
|
||||
with zipfile.ZipFile(tmp_path, 'r') as zip_ref:
|
||||
# Find the first FIT file in the zip
|
||||
fit_files = [f for f in zip_ref.namelist() if f.lower().endswith('.fit')]
|
||||
|
||||
with zip_ref.open(fit_filename) as source, open(extracted_path, 'wb') as target:
|
||||
if fit_files:
|
||||
# Extract the first FIT file
|
||||
fit_filename = fit_files[0]
|
||||
extracted_path = DATA_DIR / f"activity_{activity_id}.fit"
|
||||
|
||||
with zip_ref.open(fit_filename) as source, open(extracted_path, 'wb') as target:
|
||||
target.write(source.read())
|
||||
|
||||
# Clean up temporary zip file
|
||||
tmp_path.unlink()
|
||||
|
||||
logger.info(f"Downloaded original activity file: {extracted_path}")
|
||||
return extracted_path
|
||||
else:
|
||||
logger.warning("No FIT file found in downloaded archive")
|
||||
tmp_path.unlink()
|
||||
return None
|
||||
else:
|
||||
# Treat data as direct FIT bytes
|
||||
extracted_path = DATA_DIR / f"activity_{activity_id}.fit"
|
||||
try:
|
||||
tmp_path.rename(extracted_path)
|
||||
except Exception as move_err:
|
||||
logger.debug(f"Rename temp FIT to destination failed ({move_err}); falling back to copy")
|
||||
with open(extracted_path, 'wb') as target, open(tmp_path, 'rb') as source:
|
||||
target.write(source.read())
|
||||
|
||||
# Clean up temporary zip file
|
||||
tmp_path.unlink()
|
||||
|
||||
logger.info(f"Downloaded original activity file: {extracted_path}")
|
||||
return extracted_path
|
||||
else:
|
||||
logger.warning("No FIT file found in downloaded archive")
|
||||
tmp_path.unlink()
|
||||
return None
|
||||
logger.info(f"Downloaded original activity file: {extracted_path}")
|
||||
return extracted_path
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to download original activity {activity_id}: {e}")
|
||||
logger.error(f"Failed to download original activity {activity_id}: {e} (type={type(e).__name__})")
|
||||
return None
|
||||
|
||||
def get_activity_summary(self, activity_id: str) -> Optional[Dict[str, Any]]:
|
||||
@@ -313,4 +479,123 @@ class GarminClient:
|
||||
# Move to requested location
|
||||
downloaded_path.rename(file_path)
|
||||
return True
|
||||
return False
|
||||
return False
|
||||
def download_all_workouts(self, limit: int = 50, output_dir: Path = DATA_DIR) -> List[Dict[str, Path]]:
|
||||
"""Download up to 'limit' cycling workouts and save FIT files to output_dir.
|
||||
|
||||
Uses get_all_cycling_workouts() to list activities, then downloads each original
|
||||
activity archive and extracts the FIT file via download_activity_original().
|
||||
|
||||
Args:
|
||||
limit: Maximum number of cycling activities to download
|
||||
output_dir: Directory to save downloaded FIT files
|
||||
|
||||
Returns:
|
||||
List of dicts with 'file_path' pointing to downloaded FIT paths
|
||||
"""
|
||||
if not self.is_authenticated():
|
||||
if not self.authenticate():
|
||||
logger.error("Authentication failed; cannot download workouts")
|
||||
return []
|
||||
|
||||
try:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
activities = self.get_all_cycling_workouts(limit=limit)
|
||||
total = min(limit, len(activities))
|
||||
logger.info(f"Preparing to download up to {total} cycling activities into {output_dir}")
|
||||
|
||||
results: List[Dict[str, Path]] = []
|
||||
for idx, activity in enumerate(activities[:limit], start=1):
|
||||
activity_id = (
|
||||
activity.get("activityId")
|
||||
or activity.get("activity_id")
|
||||
or activity.get("id")
|
||||
)
|
||||
if not activity_id:
|
||||
logger.warning("Skipping activity with missing ID key (activityId/activity_id/id)")
|
||||
continue
|
||||
|
||||
logger.debug(f"Downloading activity ID {activity_id} ({idx}/{total})")
|
||||
src_path = self.download_activity_original(str(activity_id))
|
||||
if src_path and src_path.exists():
|
||||
dest_path = output_dir / src_path.name
|
||||
try:
|
||||
if src_path.resolve() != dest_path.resolve():
|
||||
if dest_path.exists():
|
||||
# Overwrite existing destination to keep most recent download
|
||||
dest_path.unlink()
|
||||
src_path.rename(dest_path)
|
||||
else:
|
||||
# Already in the desired location
|
||||
pass
|
||||
except Exception as move_err:
|
||||
logger.error(f"Failed to move {src_path} to {dest_path}: {move_err}")
|
||||
dest_path = src_path # fall back to original location
|
||||
|
||||
logger.info(f"Saved activity {activity_id} to {dest_path}")
|
||||
results.append({"file_path": dest_path})
|
||||
else:
|
||||
logger.warning(f"Download returned no file for activity {activity_id}")
|
||||
|
||||
logger.info(f"Downloaded {len(results)} activities to {output_dir}")
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed during batch download: {e}")
|
||||
return []
|
||||
|
||||
def download_latest_workout(self, output_dir: Path = DATA_DIR) -> Optional[Path]:
|
||||
"""Download the latest cycling workout and save FIT file to output_dir.
|
||||
|
||||
Uses get_latest_activity('cycling') to find the most recent cycling activity,
|
||||
then downloads the original archive and extracts the FIT via download_activity_original().
|
||||
|
||||
Args:
|
||||
output_dir: Directory to save the downloaded FIT file
|
||||
|
||||
Returns:
|
||||
Path to the downloaded FIT file or None if download failed
|
||||
"""
|
||||
if not self.is_authenticated():
|
||||
if not self.authenticate():
|
||||
logger.error("Authentication failed; cannot download latest workout")
|
||||
return None
|
||||
|
||||
try:
|
||||
latest = self.get_latest_activity(activity_type="cycling")
|
||||
if not latest:
|
||||
logger.warning("No latest cycling activity found")
|
||||
return None
|
||||
|
||||
activity_id = (
|
||||
latest.get("activityId")
|
||||
or latest.get("activity_id")
|
||||
or latest.get("id")
|
||||
)
|
||||
if not activity_id:
|
||||
logger.error("Latest activity missing ID key (activityId/activity_id/id)")
|
||||
return None
|
||||
|
||||
logger.info(f"Downloading latest cycling activity ID {activity_id}")
|
||||
src_path = self.download_activity_original(str(activity_id))
|
||||
if src_path and src_path.exists():
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
dest_path = output_dir / src_path.name
|
||||
try:
|
||||
if src_path.resolve() != dest_path.resolve():
|
||||
if dest_path.exists():
|
||||
dest_path.unlink()
|
||||
src_path.rename(dest_path)
|
||||
except Exception as move_err:
|
||||
logger.error(f"Failed to move {src_path} to {dest_path}: {move_err}")
|
||||
return src_path # Return original location if move failed
|
||||
|
||||
logger.info(f"Saved latest activity {activity_id} to {dest_path}")
|
||||
return dest_path
|
||||
|
||||
logger.warning(f"Download returned no file for latest activity {activity_id}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to download latest workout: {e}")
|
||||
return None
|
||||
7059
git_scape_garmin_analyser_digest (2).txt
Normal file
7059
git_scape_garmin_analyser_digest (2).txt
Normal file
File diff suppressed because it is too large
Load Diff
4741
git_scape_garth_digest (1).txt
Normal file
4741
git_scape_garth_digest (1).txt
Normal file
File diff suppressed because it is too large
Load Diff
31241
git_scape_python_garminconnect_digest (1).txt
Normal file
31241
git_scape_python_garminconnect_digest (1).txt
Normal file
File diff suppressed because it is too large
Load Diff
17
main.py
17
main.py
@@ -280,15 +280,26 @@ class GarminAnalyser:
|
||||
download_output_dir = Path(getattr(args, 'output_dir', 'data'))
|
||||
download_output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logging.debug(f"download_workouts: all={getattr(args, 'all', False)}, workout_id={getattr(args, 'workout_id', None)}, limit={getattr(args, 'limit', 50)}, output_dir={download_output_dir}")
|
||||
|
||||
downloaded_activities = []
|
||||
if getattr(args, 'all', False):
|
||||
logging.info(f"Downloading up to {getattr(args, 'limit', 50)} cycling activities...")
|
||||
downloaded_activities = client.download_all_workouts(limit=getattr(args, 'limit', 50), output_dir=download_output_dir)
|
||||
elif getattr(args, 'workout_id', None):
|
||||
logging.info(f"Downloading workout {args.workout_id}...")
|
||||
activity_path = client.download_activity_original(str(args.workout_id), output_dir=download_output_dir)
|
||||
activity_path = client.download_activity_original(str(args.workout_id))
|
||||
if activity_path:
|
||||
downloaded_activities.append({'file_path': activity_path})
|
||||
dest_path = download_output_dir / activity_path.name
|
||||
try:
|
||||
if activity_path.resolve() != dest_path.resolve():
|
||||
if dest_path.exists():
|
||||
dest_path.unlink()
|
||||
activity_path.rename(dest_path)
|
||||
except Exception as move_err:
|
||||
logging.error(f"Failed to move {activity_path} to {dest_path}: {move_err}")
|
||||
dest_path = activity_path
|
||||
downloaded_activities.append({'file_path': dest_path})
|
||||
else:
|
||||
logging.info("Downloading latest cycling activity...")
|
||||
activity_path = client.download_latest_workout(output_dir=download_output_dir)
|
||||
@@ -463,7 +474,7 @@ def main():
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error: {e}", file=sys.stderr)
|
||||
logging.error(f"Error: {e}")
|
||||
if args.verbose:
|
||||
logging.exception("Full traceback:")
|
||||
sys.exit(1)
|
||||
|
||||
Reference in New Issue
Block a user