mirror of
https://github.com/sstent/go-garth.git
synced 2025-12-06 08:01:42 +00:00
feat: Implement Phase 1C.3: Batch Download Features
This commit is contained in:
@@ -6,6 +6,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -38,9 +40,7 @@ var (
|
|||||||
downloadActivitiesCmd = &cobra.Command{
|
downloadActivitiesCmd = &cobra.Command{
|
||||||
Use: "download [activityID]",
|
Use: "download [activityID]",
|
||||||
Short: "Download activity data",
|
Short: "Download activity data",
|
||||||
Long: `Download activity data in various formats (e.g., GPX, TCX).`,
|
Args: cobra.RangeArgs(0, 1), RunE: runDownloadActivity,
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
RunE: runDownloadActivity,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
searchActivitiesCmd = &cobra.Command{
|
searchActivitiesCmd = &cobra.Command{
|
||||||
@@ -61,6 +61,7 @@ var (
|
|||||||
downloadFormat string
|
downloadFormat string
|
||||||
outputDir string
|
outputDir string
|
||||||
downloadOriginal bool
|
downloadOriginal bool
|
||||||
|
downloadAll bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -80,6 +81,11 @@ func init() {
|
|||||||
downloadActivitiesCmd.Flags().StringVar(&outputDir, "output-dir", ".", "Output directory for downloaded files")
|
downloadActivitiesCmd.Flags().StringVar(&outputDir, "output-dir", ".", "Output directory for downloaded files")
|
||||||
downloadActivitiesCmd.Flags().BoolVar(&downloadOriginal, "original", false, "Download original uploaded file")
|
downloadActivitiesCmd.Flags().BoolVar(&downloadOriginal, "original", false, "Download original uploaded file")
|
||||||
|
|
||||||
|
downloadActivitiesCmd.Flags().BoolVar(&downloadAll, "all", false, "Download all activities matching filters")
|
||||||
|
downloadActivitiesCmd.Flags().StringVar(&activityType, "type", "", "Filter activities by type (e.g., running, cycling)")
|
||||||
|
downloadActivitiesCmd.Flags().StringVar(&activityDateFrom, "from", "", "Start date for filtering activities (YYYY-MM-DD)")
|
||||||
|
downloadActivitiesCmd.Flags().StringVar(&activityDateTo, "to", "", "End date for filtering activities (YYYY-MM-DD)")
|
||||||
|
|
||||||
activitiesCmd.AddCommand(searchActivitiesCmd)
|
activitiesCmd.AddCommand(searchActivitiesCmd)
|
||||||
searchActivitiesCmd.Flags().StringP("query", "q", "", "Query string to search for activities")
|
searchActivitiesCmd.Flags().StringP("query", "q", "", "Query string to search for activities")
|
||||||
}
|
}
|
||||||
@@ -169,12 +175,6 @@ func runGetActivity(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runDownloadActivity(cmd *cobra.Command, args []string) error {
|
func runDownloadActivity(cmd *cobra.Command, args []string) error {
|
||||||
activityIDStr := args[0]
|
|
||||||
activityID, err := strconv.Atoi(activityIDStr)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid activity ID: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
garminClient, err := garmin.NewClient("www.garmin.com") // TODO: Domain should be configurable
|
garminClient, err := garmin.NewClient("www.garmin.com") // TODO: Domain should be configurable
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create client: %w", err)
|
return fmt.Errorf("failed to create client: %w", err)
|
||||||
@@ -185,57 +185,123 @@ func runDownloadActivity(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("not logged in: %w", err)
|
return fmt.Errorf("not logged in: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if downloadFormat == "csv" {
|
var activitiesToDownload []garmin.Activity
|
||||||
|
|
||||||
|
if downloadAll || len(args) == 0 {
|
||||||
|
opts := garmin.ActivityOptions{
|
||||||
|
ActivityType: activityType,
|
||||||
|
}
|
||||||
|
|
||||||
|
if activityDateFrom != "" {
|
||||||
|
opts.DateFrom, err = time.Parse("2006-01-02", activityDateFrom)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid date format for --from: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if activityDateTo != "" {
|
||||||
|
opts.DateTo, err = time.Parse("2006-01-02", activityDateTo)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid date format for --to: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activitiesToDownload, err = garminClient.ListActivities(opts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list activities for batch download: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(activitiesToDownload) == 0 {
|
||||||
|
fmt.Println("No activities found matching the filters for download.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else if len(args) == 1 {
|
||||||
|
activityIDStr := args[0]
|
||||||
|
activityID, err := strconv.Atoi(activityIDStr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid activity ID: %w", err)
|
||||||
|
}
|
||||||
|
// For single download, we need to fetch the activity details to get its name and type
|
||||||
activityDetail, err := garminClient.GetActivity(activityID)
|
activityDetail, err := garminClient.GetActivity(activityID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get activity details for CSV export: %w", err)
|
return fmt.Errorf("failed to get activity details for download: %w", err)
|
||||||
}
|
}
|
||||||
|
activitiesToDownload = []garmin.Activity{activityDetail.Activity}
|
||||||
filename := fmt.Sprintf("%d.csv", activityID)
|
} else {
|
||||||
outputPath := filename
|
return fmt.Errorf("invalid arguments: specify an activity ID or use --all with filters")
|
||||||
if outputDir != "" {
|
|
||||||
outputPath = filepath.Join(outputDir, filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := os.Create(outputPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create CSV file: %w", err)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
writer := csv.NewWriter(file)
|
|
||||||
defer writer.Flush()
|
|
||||||
|
|
||||||
// Write header
|
|
||||||
writer.Write([]string{"ActivityID", "ActivityName", "ActivityType", "StartTime", "Distance(km)", "Duration(s)", "Description"})
|
|
||||||
|
|
||||||
// Write data
|
|
||||||
writer.Write([]string{
|
|
||||||
fmt.Sprintf("%d", activityDetail.ActivityID),
|
|
||||||
activityDetail.ActivityName,
|
|
||||||
activityDetail.ActivityType,
|
|
||||||
activityDetail.Starttime.Format("2006-01-02 15:04:05"),
|
|
||||||
fmt.Sprintf("%.2f", activityDetail.Distance/1000),
|
|
||||||
fmt.Sprintf("%.0f", activityDetail.Duration),
|
|
||||||
activityDetail.Description,
|
|
||||||
})
|
|
||||||
|
|
||||||
fmt.Printf("Activity %d summary exported to %s\n", activityID, outputPath)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := garmin.DownloadOptions{
|
fmt.Printf("Starting download of %d activities...\n", len(activitiesToDownload))
|
||||||
Format: downloadFormat,
|
var downloadedCount int64
|
||||||
OutputDir: outputDir,
|
for _, activity := range activitiesToDownload {
|
||||||
Original: downloadOriginal,
|
wg.Add(1)
|
||||||
|
sem <- struct{}{}
|
||||||
|
go func(activity garmin.Activity) {
|
||||||
|
defer wg.Done()
|
||||||
|
defer func() { <-sem }()
|
||||||
|
|
||||||
|
if downloadFormat == "csv" {
|
||||||
|
activityDetail, err := garminClient.GetActivity(activity.ActivityID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Warning: Failed to get activity details for CSV export for activity %d: %v\n", activity.ActivityID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := fmt.Sprintf("%d.csv", activity.ActivityID)
|
||||||
|
outputPath := filename
|
||||||
|
if outputDir != "" {
|
||||||
|
outputPath = filepath.Join(outputDir, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Warning: Failed to create CSV file for activity %d: %v\n", activity.ActivityID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
writer := csv.NewWriter(file)
|
||||||
|
defer writer.Flush()
|
||||||
|
|
||||||
|
// Write header
|
||||||
|
writer.Write([]string{"ActivityID", "ActivityName", "ActivityType", "StartTime", "Distance(km)", "Duration(s)", "Description"})
|
||||||
|
|
||||||
|
// Write data
|
||||||
|
writer.Write([]string{
|
||||||
|
fmt.Sprintf("%d", activityDetail.ActivityID),
|
||||||
|
activityDetail.ActivityName,
|
||||||
|
activityDetail.ActivityType,
|
||||||
|
activityDetail.Starttime.Format("2006-01-02 15:04:05"),
|
||||||
|
fmt.Sprintf("%.2f", activityDetail.Distance/1000),
|
||||||
|
fmt.Sprintf("%.0f", activityDetail.Duration),
|
||||||
|
activityDetail.Description,
|
||||||
|
})
|
||||||
|
|
||||||
|
fmt.Printf("Activity %d summary exported to %s\n", activity.ActivityID, outputPath)
|
||||||
|
} else {
|
||||||
|
opts := garmin.DownloadOptions{
|
||||||
|
Format: downloadFormat,
|
||||||
|
OutputDir: outputDir,
|
||||||
|
Original: downloadOriginal,
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Downloading activity %d in %s format to %s...\n", activity.ActivityID, downloadFormat, outputDir)
|
||||||
|
if err := garminClient.DownloadActivity(activity.ActivityID, opts); err != nil {
|
||||||
|
fmt.Printf("Warning: Failed to download activity %d: %v\n", activity.ActivityID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Activity %d downloaded successfully.\n", activity.ActivityID)
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic.AddInt64(&downloadedCount, 1)
|
||||||
|
fmt.Printf("[%d/%d] Downloaded activity %d.\n", downloadedCount, len(activitiesToDownload), activity.ActivityID)
|
||||||
|
}(activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Downloading activity %d in %s format to %s...\n", activityID, downloadFormat, outputDir)
|
wg.Wait()
|
||||||
if err := garminClient.DownloadActivity(activityID, opts); err != nil {
|
fmt.Println("All downloads finished.")
|
||||||
return fmt.Errorf("failed to download activity: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Activity %d downloaded successfully.\n", activityID)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
phase1.md
10
phase1.md
@@ -327,15 +327,15 @@ garth activities download --from 2024-01-01 --to 2024-01-31
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Tasks:**
|
**Tasks:**
|
||||||
- [ ] Implement batch download with filtering
|
- [x] Implement batch download with filtering
|
||||||
- [ ] Add parallel download support
|
- [x] Add parallel download support
|
||||||
- [ ] Progress bars for multiple downloads
|
- [x] Progress bars for multiple downloads
|
||||||
- [ ] Resume interrupted downloads
|
- [ ] Resume interrupted downloads
|
||||||
- [ ] Duplicate detection and handling
|
- [ ] Duplicate detection and handling
|
||||||
|
|
||||||
**Deliverables:**
|
**Deliverables:**
|
||||||
- [ ] Batch download working
|
- [x] Batch download working
|
||||||
- [ ] Parallel processing implemented
|
- [x] Parallel processing implemented
|
||||||
- [ ] Resume capability
|
- [ ] Resume capability
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user