git-fit 0.3.2 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 66005a7ca77d10a5922f57d085f4354c3ec4b0838faf350b9b2533e1159b7870
4
- data.tar.gz: 783823d2e2e2b8031572527afdaaa8a5b2b06491b9a98f07671147233609c315
3
+ metadata.gz: 8bf1cd02058a384245ee15ef6471ae7847c3d104ce9774ad1c05c78d21cb2313
4
+ data.tar.gz: 7d412258ebe13b65f7f35831e6b40f4aa76fd01377b9dfa7076770c147da6bcf
5
5
  SHA512:
6
- metadata.gz: c89175b5dc5c16ac6def3cf27bb5a9ed3019f3bd0eeac32667948e36449eb3f3f2cf398a76106314be790587233396da65c7f278665c6de92284597d6870f6c4
7
- data.tar.gz: 6de6c3ed115ce17df85754c0c2d90e8ce069fc91e86593ade7061ee64acdeb3661903667680fb26a4604a4eadf57c5c9b7f8832b3bdd5648d7b15ca0aebd8091
6
+ metadata.gz: 82f3d058175b4a66b4e852ad42cccc3a119fe2e980a89179d9f21e19236dc1fec047e7a074db6014351f39c5080f61609759c7456b75d1aa2c0ac8e4c5861c35
7
+ data.tar.gz: 7d43fd945b988ffec45bd88879ce470345320a223afe82d0354a09302adf453c66dbf6aea8da78ec9c6c022e701a985b958164bfa691073efd67a79e814f90ad
data/lib/git-fit.rb CHANGED
@@ -51,11 +51,16 @@ require_relative "git_fit/sport_mapper"
51
51
  require_relative "git_fit/geo"
52
52
  require_relative "git_fit/fit"
53
53
  require_relative "git_fit/parser"
54
+ require_relative "git_fit/source"
54
55
  require_relative "git_fit/timezone/resolver"
55
56
  require_relative "git_fit/privacy/polyline_filter"
57
+ require_relative "git_fit/import"
56
58
  require_relative "git_fit/sync/base"
57
59
  require_relative "git_fit/sync/strava"
58
60
  require_relative "git_fit/db/connection"
59
61
  require_relative "git_fit/db/activity"
62
+ require_relative "git_fit/export"
60
63
  require_relative "git_fit/db/cli"
64
+ require_relative "git_fit/cli/export"
65
+ require_relative "git_fit/cli/import_cli"
61
66
  require_relative "git_fit/cli"
@@ -0,0 +1,25 @@
1
+ require "thor"
2
+
3
+ module GitFit
4
+ class ExportCLI < Thor
5
+ desc "json", "Export activities to JSON"
6
+ option :output, type: :string, aliases: "-o", desc: "Output path"
7
+ option :activity, type: :array, desc: "Filter by sport type", aliases: "--act"
8
+ no_commands do
9
+ def git_fit_config
10
+ @git_fit_config ||= GitFit::Config.new(options[:config])
11
+ end
12
+ end
13
+
14
+ def json
15
+ config = git_fit_config
16
+ db = GitFit::DB::Connection.new(config.db_path).db
17
+ path = options[:output] || config.export_config.dig("json", "path") || "site/activities.json"
18
+ filter = options[:activity]&.map(&:strip)&.map(&:downcase)
19
+ count = Export::JSON.new(db: db, output: path, activity_filter: filter).call
20
+ say_status :done, "Exported #{count} activities to #{path}", :green
21
+ rescue => e
22
+ say_status :error, "JSON export failed: #{e.message}", :red
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,169 @@
1
+ require "thor"
2
+
3
+ module GitFit
4
+ class ImportCLI < Thor
5
+ FILE_SOURCES = %w[gpx fit tcx].freeze
6
+
7
+ desc "all", "Import from all sources (gpx, fit, tcx)"
8
+ option :source, type: :string, aliases: "-s",
9
+ desc: "Import source(s) — comma-separated (default: gpx,fit,tcx)"
10
+ option :activity, type: :array, desc: "Filter by sport type", aliases: "--act"
11
+ option :sport, type: :string, desc: "Override sport_category for GPX files"
12
+ def all
13
+ sources = (options[:source] || "gpx,fit,tcx").split(",").map(&:strip).map(&:downcase)
14
+ unknown = sources.reject { |s| FILE_SOURCES.include?(s) }
15
+ unless unknown.empty?
16
+ say_status :error, "Unknown source(s): #{unknown.join(', ')}. Supported: gpx, fit, tcx", :red
17
+ return
18
+ end
19
+
20
+ config = GitFit::Config.new
21
+ db = GitFit::DB::Connection.new(config.db_path).db
22
+ filter = options[:activity]&.map(&:strip)&.map(&:downcase)
23
+ time_budget = config.dig("sync", "time_budget")
24
+ sport_override = options[:sport]
25
+
26
+ total = 0
27
+ sources.each do |source|
28
+ say_status :import, source, :green
29
+ adapter = GitFit::Import::LocalFile.new(
30
+ config: {}, db:, activity_filter: filter,
31
+ time_budget: time_budget,
32
+ sources: [source],
33
+ sport_override: sport_override
34
+ )
35
+ count = adapter.call
36
+ say_status :done, "#{source}: #{count} activities", :green
37
+ total += count
38
+ rescue => e
39
+ say_status :error, "#{source}: #{e.message}", :red
40
+ end
41
+
42
+ if total.zero?
43
+ say_status :warn, "0 activities imported", :yellow
44
+ else
45
+ say_status :done, "Total: #{total} activities", :green
46
+ end
47
+ end
48
+
49
+ default_command :all
50
+
51
+ desc "gpx", "Import GPX files from data/import/gpx/"
52
+ option :activity, type: :array, desc: "Filter by sport type", aliases: "--act"
53
+ option :sport, type: :string, desc: "Override sport_category for GPX files"
54
+ def gpx
55
+ config = GitFit::Config.new
56
+ db = GitFit::DB::Connection.new(config.db_path).db
57
+ filter = options[:activity]&.map(&:strip)&.map(&:downcase)
58
+ time_budget = config.dig("sync", "time_budget")
59
+ sport_override = options[:sport]
60
+
61
+ say_status :import, "gpx", :green
62
+ adapter = GitFit::Import::LocalFile.new(
63
+ config: {}, db:, activity_filter: filter,
64
+ time_budget: time_budget,
65
+ sources: ["gpx"],
66
+ sport_override: sport_override
67
+ )
68
+
69
+ begin
70
+ count = adapter.call
71
+ if count == 0
72
+ say_status :warn, "gpx: 0 activities (already imported or none found)", :yellow
73
+ else
74
+ say_status :done, "gpx: #{count} activities", :green
75
+ end
76
+ count
77
+ rescue => e
78
+ say_status :error, "gpx: #{e.message}", :red
79
+ 0
80
+ end
81
+
82
+ desc "tcx", "Import TCX files from data/import/tcx/"
83
+ option :activity, type: :array, desc: "Filter by sport type", aliases: "--act"
84
+ def tcx
85
+ config = GitFit::Config.new
86
+ db = GitFit::DB::Connection.new(config.db_path).db
87
+ filter = options[:activity]&.map(&:strip)&.map(&:downcase)
88
+ time_budget = config.dig("sync", "time_budget")
89
+
90
+ say_status :import, "tcx", :green
91
+ adapter = GitFit::Import::LocalFile.new(
92
+ config: {}, db:, activity_filter: filter,
93
+ time_budget: time_budget,
94
+ sources: ["tcx"]
95
+ )
96
+
97
+ begin
98
+ count = adapter.call
99
+ if count == 0
100
+ say_status :warn, "tcx: 0 activities (already imported or none found)", :yellow
101
+ else
102
+ say_status :done, "tcx: #{count} activities", :green
103
+ end
104
+ count
105
+ rescue => e
106
+ say_status :error, "tcx: #{e.message}", :red
107
+ 0
108
+ end
109
+ end
110
+
111
+ desc "fit", "Import FIT files from data/import/fit/"
112
+ option :activity, type: :array, desc: "Filter by sport type", aliases: "--act"
113
+ def fit
114
+ config = GitFit::Config.new
115
+ db = GitFit::DB::Connection.new(config.db_path).db
116
+ filter = options[:activity]&.map(&:strip)&.map(&:downcase)
117
+ time_budget = config.dig("sync", "time_budget")
118
+
119
+ say_status :import, "fit", :green
120
+ adapter = GitFit::Import::LocalFile.new(
121
+ config: {}, db:, activity_filter: filter,
122
+ time_budget: time_budget,
123
+ sources: ["fit"]
124
+ )
125
+
126
+ begin
127
+ count = adapter.call
128
+ if count == 0
129
+ say_status :warn, "fit: 0 activities (already imported or none found)", :yellow
130
+ else
131
+ say_status :done, "fit: #{count} activities", :green
132
+ end
133
+ count
134
+ rescue => e
135
+ say_status :error, "fit: #{e.message}", :red
136
+ 0
137
+ end
138
+ end
139
+
140
+ desc "apple_health", "Import from Apple Health export.zip"
141
+ option :activity, type: :array, desc: "Filter by sport type", aliases: "--act"
142
+ def apple_health
143
+ config = GitFit::Config.new
144
+ db = GitFit::DB::Connection.new(config.db_path).db
145
+ filter = options[:activity]&.map(&:strip)&.map(&:downcase)
146
+ time_budget = config.dig("sync", "time_budget")
147
+
148
+ say_status :import, "apple_health", :green
149
+ adapter = GitFit::Import::AppleHealth.new(
150
+ config: {}, db:, activity_filter: filter,
151
+ time_budget: time_budget
152
+ )
153
+
154
+ begin
155
+ count = adapter.call
156
+ if count == 0
157
+ say_status :warn, "apple_health: 0 activities (already imported or none found)", :yellow
158
+ else
159
+ say_status :done, "apple_health: #{count} activities", :green
160
+ end
161
+ count
162
+ rescue => e
163
+ say_status :error, "apple_health: #{e.message}", :red
164
+ 0
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
data/lib/git_fit/cli.rb CHANGED
@@ -13,6 +13,12 @@ module GitFit
13
13
  desc "db SUBCOMMAND", "Database operations"
14
14
  subcommand "db", DB::CLI
15
15
 
16
+ desc "import SUBCOMMAND", "Import activities from local files"
17
+ subcommand "import", ImportCLI
18
+
19
+ desc "export SUBCOMMAND", "Export activities to various formats"
20
+ subcommand "export", ExportCLI
21
+
16
22
  desc "version", "Show version"
17
23
  def version
18
24
  puts "git-fit v#{GitFit::VERSION}"
@@ -0,0 +1,29 @@
1
+ module GitFit
2
+ module Export
3
+ module Defaults
4
+ SVG = {
5
+ background: "#222222", text: "#FFFFFF", track: "#4DD2FF",
6
+ track2: "#4DD2FF", special: "#FFFF00", special2: "#FFFF00",
7
+ empty_cell: "#444444", dim_past: "#555555", font: "Arial",
8
+ }.freeze
9
+
10
+ DASHBOARD = {
11
+ background: "#1a1a2e", surface: "#16213e", hover: "#0f3460",
12
+ active_row: "#1a3a6a", border: "#2a2a4a", text: "#e0e0e0",
13
+ text_secondary: "#888", text_dim: "#666", text_insight: "#aaa",
14
+ accent: "#4DD2FF", font: "-apple-system, BlinkMacSystemFont, sans-serif",
15
+ }.freeze
16
+
17
+ CATEGORIES = {
18
+ "run" => "#4DD2FF", "walk" => "#4ECDC4",
19
+ "ride" => "#FF6B6B", "hike" => "#FFD93D",
20
+ }.freeze
21
+
22
+ MAP = {
23
+ tiles_dark: "https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png",
24
+ tiles_light: "https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png",
25
+ echarts_theme: "dark",
26
+ }.freeze
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,112 @@
1
+ require "json"
2
+
3
+ module GitFit
4
+ module Export
5
+ class JSON
6
+ PLACEHOLDER_POLYLINE = "gqqrFurkeU??".freeze
7
+
8
+ def initialize(db:, output:, activity_filter: nil)
9
+ @db = db
10
+ @output = output
11
+ @activity_filter = activity_filter
12
+ end
13
+
14
+ def call
15
+ dataset = @db[:activities].order(Sequel.desc(:start_date))
16
+ dataset = dataset.where(sport_category: @activity_filter) if @activity_filter
17
+
18
+ activities = dataset.all
19
+
20
+ run_id_to_group = {}
21
+ @db[:dedup_group_members].each { |gm| run_id_to_group[gm[:run_id]] = gm[:group_id] }
22
+
23
+ group_members_cache = {}
24
+
25
+ formatted = activities.map do |a|
26
+ gid = run_id_to_group[a[:run_id]]
27
+ if gid
28
+ group_members_cache[gid] ||= @db[:dedup_group_members].where(group_id: gid).all
29
+ composite_activity(a, gid, group_members_cache[gid])
30
+ else
31
+ format_activity(a)
32
+ end
33
+ end
34
+
35
+ File.write(@output, ::JSON.pretty_generate(formatted))
36
+ formatted.size
37
+ end
38
+
39
+ private
40
+
41
+ def format_activity(a)
42
+ base = {
43
+ run_id: a[:run_id],
44
+ name: a[:name],
45
+ distance: a[:distance],
46
+ moving_time: a[:moving_time],
47
+ type: a[:sport_category],
48
+ sport_type: a[:sport_type],
49
+ start_date: a[:start_date],
50
+ start_date_local: a[:start_date_local],
51
+ location_country: a[:location_country],
52
+ summary_polyline: a[:source] == "keep" && a[:summary_polyline] == PLACEHOLDER_POLYLINE ?
53
+ nil : a[:summary_polyline],
54
+ average_heartrate: a[:average_heartrate] && a[:average_heartrate] != 0 ? a[:average_heartrate] : nil,
55
+ max_heartrate: a[:max_heartrate],
56
+ average_cadence: a[:average_cadence],
57
+ max_cadence: a[:max_cadence],
58
+ average_power: a[:average_power],
59
+ max_power: a[:max_power],
60
+ calories: a[:calories],
61
+ average_temperature: a[:average_temperature],
62
+ average_speed: a[:average_speed],
63
+ elevation_gain: a[:elevation_gain],
64
+ source: a[:source],
65
+ }
66
+ if a[:divisions]
67
+ base[:divisions] = ::JSON.parse(a[:divisions])
68
+ end
69
+ base
70
+ end
71
+
72
+ def composite_activity(activity, group_id, members)
73
+ run_ids = members.map { |m| m[:run_id] }
74
+ group_activities = @db[:activities].where(run_id: run_ids).all
75
+
76
+ composite = if defined?(GitFit::Dedup::FieldElector)
77
+ elector = GitFit::Dedup::FieldElector.new
78
+ result = {}
79
+ GitFit::Dedup::FieldElector::FIELD_ELECTION_RULES.each_key do |field|
80
+ values = group_activities.map { |a| a[field] }
81
+ sources = group_activities.map { |a| a[:source]&.to_sym }
82
+ result[field] = elector.elect(field, values, sources)
83
+ end
84
+ result
85
+ else
86
+ members.first&.slice(:run_id, :distance, :moving_time, :elapsed_time,
87
+ :average_heartrate, :max_heartrate, :elevation_gain,
88
+ :average_speed, :average_cadence, :max_cadence) || {}
89
+ end
90
+
91
+ composite[:run_id] = activity[:run_id]
92
+ composite[:source] = activity[:source]
93
+ composite[:sport_type] = activity[:sport_type]
94
+ composite[:start_date_local] = activity[:start_date_local]
95
+ composite[:location_country] = activity[:location_country]
96
+ composite[:sport_category] = activity[:sport_category]
97
+
98
+ if composite[:distance].to_f > 0 && composite[:moving_time].to_f > 0
99
+ composite[:average_speed] = composite[:distance] / composite[:moving_time]
100
+ end
101
+
102
+ composite[:type] = composite[:sport_category]
103
+ composite[:dedup_group] = group_id
104
+ composite[:member_sources] = members.map do |m|
105
+ { source: m[:source], run_id: m[:run_id], flag: m[:flag], confidence: m[:confidence] }
106
+ end
107
+
108
+ composite
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,7 @@
1
+ module GitFit
2
+ module Export
3
+ end
4
+ end
5
+
6
+ require_relative "export/defaults"
7
+ require_relative "export/json"