mirror of
https://github.com/sstent/garminsync-go.git
synced 2025-12-06 08:01:52 +00:00
348 lines
10 KiB
Go
348 lines
10 KiB
Go
// internal/database/sqlite.go
|
|
package database
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type SQLiteDB struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
func NewSQLiteDB(dbPath string) (*SQLiteDB, error) {
|
|
db, err := sql.Open("sqlite3", dbPath+"?_foreign_keys=on")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sqlite := &SQLiteDB{db: db}
|
|
|
|
// Create tables
|
|
if err := sqlite.createTables(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return sqlite, nil
|
|
}
|
|
|
|
func (s *SQLiteDB) createTables() error {
|
|
schema := `
|
|
CREATE TABLE IF NOT EXISTS activities (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
activity_id INTEGER UNIQUE NOT NULL,
|
|
start_time DATETIME NOT NULL,
|
|
activity_type TEXT,
|
|
duration INTEGER,
|
|
distance REAL,
|
|
max_heart_rate INTEGER,
|
|
avg_heart_rate INTEGER,
|
|
avg_power REAL,
|
|
calories INTEGER,
|
|
steps INTEGER,
|
|
elevation_gain REAL,
|
|
start_latitude REAL,
|
|
start_longitude REAL,
|
|
filename TEXT UNIQUE,
|
|
file_type TEXT,
|
|
file_size INTEGER,
|
|
downloaded BOOLEAN DEFAULT FALSE,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
last_sync DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_activities_activity_id ON activities(activity_id);
|
|
CREATE INDEX IF NOT EXISTS idx_activities_start_time ON activities(start_time);
|
|
CREATE INDEX IF NOT EXISTS idx_activities_activity_type ON activities(activity_type);
|
|
CREATE INDEX IF NOT EXISTS idx_activities_downloaded ON activities(downloaded);
|
|
|
|
CREATE TABLE IF NOT EXISTS daemon_config (
|
|
id INTEGER PRIMARY KEY DEFAULT 1,
|
|
enabled BOOLEAN DEFAULT TRUE,
|
|
schedule_cron TEXT DEFAULT '0 * * * *',
|
|
last_run TEXT,
|
|
status TEXT DEFAULT 'stopped',
|
|
CONSTRAINT single_config CHECK (id = 1)
|
|
);
|
|
|
|
INSERT OR IGNORE INTO daemon_config (id) VALUES (1);
|
|
`
|
|
|
|
_, err := s.db.Exec(schema)
|
|
return err
|
|
}
|
|
|
|
func (s *SQLiteDB) GetActivities(limit, offset int) ([]Activity, error) {
|
|
query := `
|
|
SELECT id, activity_id, start_time, activity_type, duration, distance,
|
|
max_heart_rate, avg_heart_rate, avg_power, calories, steps,
|
|
elevation_gain, start_latitude, start_longitude,
|
|
filename, file_type, file_size, downloaded, created_at, last_sync
|
|
FROM activities
|
|
ORDER BY start_time DESC
|
|
LIMIT ? OFFSET ?`
|
|
|
|
rows, err := s.db.Query(query, limit, offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var activities []Activity
|
|
for rows.Next() {
|
|
var a Activity
|
|
var startTime, createdAt, lastSync string
|
|
|
|
err := rows.Scan(
|
|
&a.ID, &a.ActivityID, &startTime, &a.ActivityType,
|
|
&a.Duration, &a.Distance, &a.MaxHeartRate, &a.AvgHeartRate,
|
|
&a.AvgPower, &a.Calories, &a.Steps, &a.ElevationGain,
|
|
&a.StartLatitude, &a.StartLongitude,
|
|
&a.Filename, &a.FileType, &a.FileSize, &a.Downloaded,
|
|
&createdAt, &lastSync,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Parse time strings
|
|
if a.StartTime, err = time.Parse("2006-01-02 15:04:05", startTime); err != nil {
|
|
return nil, err
|
|
}
|
|
if a.CreatedAt, err = time.Parse("2006-01-02 15:04:05", createdAt); err != nil {
|
|
return nil, err
|
|
}
|
|
if a.LastSync, err = time.Parse("2006-01-02 15:04:05", lastSync); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
activities = append(activities, a)
|
|
}
|
|
|
|
return activities, nil
|
|
}
|
|
|
|
func (s *SQLiteDB) GetActivity(activityID int) (*Activity, error) {
|
|
query := `
|
|
SELECT id, activity_id, start_time, activity_type, duration, distance,
|
|
max_heart_rate, avg_heart_rate, avg_power, calories, steps,
|
|
elevation_gain, start_latitude, start_longitude,
|
|
filename, file_type, file_size, downloaded, created_at, last_sync
|
|
FROM activities
|
|
WHERE activity_id = ?`
|
|
|
|
row := s.db.QueryRow(query, activityID)
|
|
|
|
var a Activity
|
|
var startTime, createdAt, lastSync string
|
|
|
|
err := row.Scan(
|
|
&a.ID, &a.ActivityID, &startTime, &a.ActivityType,
|
|
&a.Duration, &a.Distance, &a.MaxHeartRate, &a.AvgHeartRate,
|
|
&a.AvgPower, &a.Calories, &a.Steps, &a.ElevationGain,
|
|
&a.StartLatitude, &a.StartLongitude,
|
|
&a.Filename, &a.FileType, &a.FileSize, &a.Downloaded,
|
|
&createdAt, &lastSync,
|
|
)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("activity not found")
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// Parse time strings
|
|
if a.StartTime, err = time.Parse("2006-01-02 15:04:05", startTime); err != nil {
|
|
return nil, err
|
|
}
|
|
if a.CreatedAt, err = time.Parse("2006-01-02 15:04:05", createdAt); err != nil {
|
|
return nil, err
|
|
}
|
|
if a.LastSync, err = time.Parse("2006-01-02 15:04:05", lastSync); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &a, nil
|
|
}
|
|
|
|
func (s *SQLiteDB) CreateActivity(activity *Activity) error {
|
|
query := `
|
|
INSERT INTO activities (
|
|
activity_id, start_time, activity_type, duration, distance,
|
|
max_heart_rate, avg_heart_rate, avg_power, calories,
|
|
steps, elevation_gain, start_latitude, start_longitude,
|
|
filename, file_type, file_size, downloaded
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
|
|
_, err := s.db.Exec(query,
|
|
activity.ActivityID, activity.StartTime.Format("2006-01-02 15:04:05"),
|
|
activity.ActivityType, activity.Duration, activity.Distance,
|
|
activity.MaxHeartRate, activity.AvgHeartRate, activity.AvgPower,
|
|
activity.Calories, activity.Steps, activity.ElevationGain,
|
|
activity.StartLatitude, activity.StartLongitude,
|
|
activity.Filename, activity.FileType,
|
|
activity.FileSize, activity.Downloaded,
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
func (s *SQLiteDB) UpdateActivity(activity *Activity) error {
|
|
query := `
|
|
UPDATE activities SET
|
|
activity_type = ?, duration = ?, distance = ?,
|
|
max_heart_rate = ?, avg_heart_rate = ?, avg_power = ?,
|
|
calories = ?, steps = ?, elevation_gain = ?,
|
|
start_latitude = ?, start_longitude = ?,
|
|
filename = ?, file_type = ?, file_size = ?,
|
|
downloaded = ?, last_sync = CURRENT_TIMESTAMP
|
|
WHERE activity_id = ?`
|
|
|
|
_, err := s.db.Exec(query,
|
|
activity.ActivityType, activity.Duration, activity.Distance,
|
|
activity.MaxHeartRate, activity.AvgHeartRate, activity.AvgPower,
|
|
activity.Calories, activity.Steps, activity.ElevationGain,
|
|
activity.StartLatitude, activity.StartLongitude,
|
|
activity.Filename, activity.FileType,
|
|
activity.FileSize, activity.Downloaded, activity.ActivityID,
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
func (s *SQLiteDB) GetStats() (*Stats, error) {
|
|
stats := &Stats{}
|
|
|
|
// Get total count
|
|
err := s.db.QueryRow("SELECT COUNT(*) FROM activities").Scan(&stats.Total)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get downloaded count
|
|
err = s.db.QueryRow("SELECT COUNT(*) FROM activities WHERE downloaded = TRUE").Scan(&stats.Downloaded)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stats.Missing = stats.Total - stats.Downloaded
|
|
|
|
return stats, nil
|
|
}
|
|
|
|
func (s *SQLiteDB) FilterActivities(filters ActivityFilters) ([]Activity, error) {
|
|
query := `
|
|
SELECT id, activity_id, start_time, activity_type, duration, distance,
|
|
max_heart_rate, avg_heart_rate, avg_power, calories, steps,
|
|
elevation_gain, start_latitude, start_longitude,
|
|
filename, file_type, file_size, downloaded, created_at, last_sync
|
|
FROM activities WHERE 1=1`
|
|
|
|
var args []interface{}
|
|
var conditions []string
|
|
|
|
// Build WHERE conditions
|
|
if filters.ActivityType != "" {
|
|
conditions = append(conditions, "activity_type = ?")
|
|
args = append(args, filters.ActivityType)
|
|
}
|
|
|
|
if filters.DateFrom != nil {
|
|
conditions = append(conditions, "start_time >= ?")
|
|
args = append(args, filters.DateFrom.Format("2006-01-02 15:04:05"))
|
|
}
|
|
|
|
if filters.DateTo != nil {
|
|
conditions = append(conditions, "start_time <= ?")
|
|
args = append(args, filters.DateTo.Format("2006-01-02 15:04:05"))
|
|
}
|
|
|
|
if filters.MinDistance > 0 {
|
|
conditions = append(conditions, "distance >= ?")
|
|
args = append(args, filters.MinDistance)
|
|
}
|
|
|
|
if filters.MaxDistance > 0 {
|
|
conditions = append(conditions, "distance <= ?")
|
|
args = append(args, filters.MaxDistance)
|
|
}
|
|
|
|
if filters.Downloaded != nil {
|
|
conditions = append(conditions, "downloaded = ?")
|
|
args = append(args, *filters.Downloaded)
|
|
}
|
|
|
|
// Add conditions to query
|
|
if len(conditions) > 0 {
|
|
query += " AND " + strings.Join(conditions, " AND ")
|
|
}
|
|
|
|
// Add sorting
|
|
orderBy := "start_time"
|
|
if filters.SortBy != "" {
|
|
orderBy = filters.SortBy
|
|
}
|
|
|
|
order := "DESC"
|
|
if filters.SortOrder == "asc" {
|
|
order = "ASC"
|
|
}
|
|
|
|
query += fmt.Sprintf(" ORDER BY %s %s", orderBy, order)
|
|
|
|
// Add pagination
|
|
if filters.Limit > 0 {
|
|
query += " LIMIT ?"
|
|
args = append(args, filters.Limit)
|
|
|
|
if filters.Offset > 0 {
|
|
query += " OFFSET ?"
|
|
args = append(args, filters.Offset)
|
|
}
|
|
}
|
|
|
|
rows, err := s.db.Query(query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var activities []Activity
|
|
for rows.Next() {
|
|
var a Activity
|
|
var startTime, createdAt, lastSync string
|
|
|
|
err := rows.Scan(
|
|
&a.ID, &a.ActivityID, &startTime, &a.ActivityType,
|
|
&a.Duration, &a.Distance, &a.MaxHeartRate, &a.AvgHeartRate,
|
|
&a.AvgPower, &a.Calories, &a.Steps, &a.ElevationGain,
|
|
&a.StartLatitude, &a.StartLongitude,
|
|
&a.Filename, &a.FileType, &a.FileSize, &a.Downloaded,
|
|
&createdAt, &lastSync,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Parse times
|
|
a.StartTime, _ = time.Parse("2006-01-02 15:04:05", startTime)
|
|
a.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
|
|
a.LastSync, _ = time.Parse("2006-01-02 15:04:05", lastSync)
|
|
|
|
activities = append(activities, a)
|
|
}
|
|
|
|
return activities, nil
|
|
}
|
|
|
|
func (s *SQLiteDB) Close() error {
|
|
return s.db.Close()
|
|
}
|
|
|
|
// NewSQLiteDBFromDB wraps an existing sql.DB connection
|
|
func NewSQLiteDBFromDB(db *sql.DB) *SQLiteDB {
|
|
return &SQLiteDB{db: db}
|
|
}
|