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 +4 -4
- data/lib/git-fit.rb +5 -0
- data/lib/git_fit/cli/export.rb +25 -0
- data/lib/git_fit/cli/import_cli.rb +169 -0
- data/lib/git_fit/cli.rb +6 -0
- data/lib/git_fit/export/defaults.rb +29 -0
- data/lib/git_fit/export/json.rb +112 -0
- data/lib/git_fit/export.rb +7 -0
- data/lib/git_fit/import/apple_health.rb +582 -0
- data/lib/git_fit/import/local_file.rb +250 -0
- data/lib/git_fit/import.rb +2 -0
- data/lib/git_fit/source/base.rb +155 -0
- data/lib/git_fit/source/tally.rb +55 -0
- data/lib/git_fit/source.rb +7 -0
- data/lib/git_fit/version.rb +1 -1
- metadata +40 -1
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
require "digest"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require "json"
|
|
4
|
+
require "nokogiri"
|
|
5
|
+
require "tzinfo"
|
|
6
|
+
|
|
7
|
+
module GitFit
|
|
8
|
+
module Import
|
|
9
|
+
class LocalFile < Source::Base
|
|
10
|
+
IMPORT_DIRS = {
|
|
11
|
+
"gpx" => "data/import/gpx",
|
|
12
|
+
"tcx" => "data/import/tcx",
|
|
13
|
+
"fit" => "data/import/fit"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
SUPPORTED_EXTENSIONS = %w[.gpx .tcx .fit].freeze
|
|
17
|
+
|
|
18
|
+
NS_TCX = { tcx: "http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2" }.freeze
|
|
19
|
+
|
|
20
|
+
def initialize(config:, db:, activity_filter: nil, privacy: nil, time_budget: nil, sources: nil, sport_override: nil)
|
|
21
|
+
super(config:, db:, activity_filter:, privacy:, time_budget:)
|
|
22
|
+
@sources = sources
|
|
23
|
+
@sport_override = sport_override
|
|
24
|
+
@timezone_resolver = GitFit::Timezone::Resolver.new(config)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def call
|
|
28
|
+
return 0 unless authenticate
|
|
29
|
+
|
|
30
|
+
total_imported = 0
|
|
31
|
+
@sources.each do |source|
|
|
32
|
+
dir = IMPORT_DIRS[source]
|
|
33
|
+
next unless dir && File.directory?(dir)
|
|
34
|
+
|
|
35
|
+
files = Dir.glob(File.join(dir, "*"), File::FNM_CASEFOLD)
|
|
36
|
+
.select { |f| SUPPORTED_EXTENSIONS.include?(File.extname(f).downcase) && File.file?(f) }
|
|
37
|
+
|
|
38
|
+
total_imported += process_files(files, source)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
total_imported
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def authenticate
|
|
45
|
+
true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def pending_ids
|
|
49
|
+
[]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def source_label
|
|
53
|
+
"LocalFile"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def raw_dir_for(source)
|
|
57
|
+
File.join("data", "raw", source)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def std_dir_for(source)
|
|
61
|
+
File.join("data", "std", source)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def process_files(files, source)
|
|
67
|
+
total_activities = 0
|
|
68
|
+
total = files.size
|
|
69
|
+
|
|
70
|
+
files.each_with_index do |file_path, idx|
|
|
71
|
+
break if time_expired? && total_activities.positive?
|
|
72
|
+
|
|
73
|
+
result = process_single_file(file_path, source)
|
|
74
|
+
if result
|
|
75
|
+
activity_count = result[:activity_count] || 1
|
|
76
|
+
total_activities += activity_count
|
|
77
|
+
report_progress(idx + 1, total, "#{activity_count} act from #{File.basename(file_path)}")
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
total_activities
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def process_single_file(file_path, source)
|
|
85
|
+
content = File.binread(file_path)
|
|
86
|
+
sha = Digest::SHA256.hexdigest(content)
|
|
87
|
+
platform_id = "#{source}_#{sha[0, 12]}"
|
|
88
|
+
|
|
89
|
+
return nil if existing_run_id?(platform_id)
|
|
90
|
+
|
|
91
|
+
ext = File.extname(file_path).downcase
|
|
92
|
+
raw_source_dir = raw_dir_for(source)
|
|
93
|
+
|
|
94
|
+
raw_path = File.join(raw_source_dir, "#{platform_id}#{ext}")
|
|
95
|
+
FileUtils.mkdir_p(raw_source_dir)
|
|
96
|
+
tmp = "#{raw_path}.tmp"
|
|
97
|
+
File.binwrite(tmp, content)
|
|
98
|
+
File.rename(tmp, raw_path)
|
|
99
|
+
|
|
100
|
+
activities = parse_file(ext, content, file_path)
|
|
101
|
+
return nil if activities.empty?
|
|
102
|
+
|
|
103
|
+
l2_points = extract_l2_points(ext, content, file_path)
|
|
104
|
+
|
|
105
|
+
imported = 0
|
|
106
|
+
activities.each do |activity|
|
|
107
|
+
activity[:source] = source
|
|
108
|
+
activity[:run_id] = imported.zero? ? platform_id : "#{platform_id}_#{imported}"
|
|
109
|
+
|
|
110
|
+
if source == "gpx" && @sport_override
|
|
111
|
+
activity[:sport_category] = SportMapper.canonicalize(@sport_override)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
if l2_points&.any?
|
|
115
|
+
std_source_dir = std_dir_for(source)
|
|
116
|
+
FileUtils.mkdir_p(std_source_dir)
|
|
117
|
+
std_data = l2_points.map { |pt|
|
|
118
|
+
h = { latitude: pt[0], longitude: pt[1] }
|
|
119
|
+
h[:altitude] = pt[2] if pt[2]
|
|
120
|
+
h[:timestamp] = pt[3]&.iso8601 if pt[3]
|
|
121
|
+
h[:heart_rate] = pt[4] if pt[4]
|
|
122
|
+
h
|
|
123
|
+
}
|
|
124
|
+
std_tmp = File.join(std_source_dir, ".#{activity[:run_id]}.json.tmp")
|
|
125
|
+
std_final = File.join(std_source_dir, "#{activity[:run_id]}.json")
|
|
126
|
+
File.write(std_tmp, JSON.generate(std_data))
|
|
127
|
+
File.rename(std_tmp, std_final)
|
|
128
|
+
|
|
129
|
+
coords = l2_points.map { |p| [p[0], p[1]] }
|
|
130
|
+
activity[:summary_polyline] = GitFit::Geo::Polyline.encode(coords)
|
|
131
|
+
|
|
132
|
+
first_pt = l2_points.first
|
|
133
|
+
tz = @timezone_resolver.resolve(first_pt[0], first_pt[1])
|
|
134
|
+
if tz && activity[:start_date]
|
|
135
|
+
activity[:start_date_local] = utc_to_local(activity[:start_date], tz)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
upsert_activity(activity)
|
|
140
|
+
imported += 1
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
{ activity_count: imported }
|
|
144
|
+
rescue => e
|
|
145
|
+
$stderr.puts "LocalFile: error importing #{file_path}: #{e.message}"
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def parse_file(ext, content, file_path)
|
|
150
|
+
case ext
|
|
151
|
+
when ".gpx"
|
|
152
|
+
GitFit::Parser::GPX.new.call(content)
|
|
153
|
+
when ".tcx"
|
|
154
|
+
GitFit::Parser::TCX.new.call(content)
|
|
155
|
+
when ".fit"
|
|
156
|
+
GitFit::Parser::FIT.new.call(file_path)
|
|
157
|
+
else
|
|
158
|
+
[]
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def extract_l2_points(ext, content, file_path)
|
|
163
|
+
case ext
|
|
164
|
+
when ".gpx"
|
|
165
|
+
extract_gpx_points(content)
|
|
166
|
+
when ".tcx"
|
|
167
|
+
extract_tcx_points(content)
|
|
168
|
+
when ".fit"
|
|
169
|
+
extract_fit_points(file_path)
|
|
170
|
+
else
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def extract_gpx_points(xml)
|
|
176
|
+
doc = Nokogiri::XML(xml)
|
|
177
|
+
doc.remove_namespaces!
|
|
178
|
+
doc.xpath("//trkpt").filter_map do |pt|
|
|
179
|
+
lat = pt["lat"]&.to_f
|
|
180
|
+
lon = pt["lon"]&.to_f
|
|
181
|
+
next unless lat && lon
|
|
182
|
+
ele = pt.at_xpath("ele")&.text&.to_f
|
|
183
|
+
time_str = pt.at_xpath("time")&.text
|
|
184
|
+
time = parse_timestamp(time_str)
|
|
185
|
+
hr = pt.at_xpath("extensions//hr")&.text&.to_f
|
|
186
|
+
[lat, lon, ele, time, hr]
|
|
187
|
+
end
|
|
188
|
+
rescue => e
|
|
189
|
+
$stderr.puts "LocalFile: GPX parse error: #{e.message}"
|
|
190
|
+
nil
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def extract_tcx_points(xml)
|
|
194
|
+
doc = Nokogiri::XML(xml)
|
|
195
|
+
doc.xpath("//tcx:Trackpoint", NS_TCX).filter_map do |tp|
|
|
196
|
+
pos = tp.at_xpath("tcx:Position", NS_TCX)
|
|
197
|
+
next unless pos
|
|
198
|
+
lat = pos.at_xpath("tcx:LatitudeDegrees", NS_TCX)&.text&.to_f
|
|
199
|
+
lon = pos.at_xpath("tcx:LongitudeDegrees", NS_TCX)&.text&.to_f
|
|
200
|
+
next unless lat && lon
|
|
201
|
+
ele = tp.at_xpath("tcx:AltitudeMeters", NS_TCX)&.text&.to_f
|
|
202
|
+
time_str = tp.at_xpath("tcx:Time", NS_TCX)&.text
|
|
203
|
+
time = parse_timestamp(time_str)
|
|
204
|
+
hr = tp.at_xpath("tcx:HeartRateBpm/tcx:Value", NS_TCX)&.text&.to_f
|
|
205
|
+
[lat, lon, ele, time, hr]
|
|
206
|
+
end
|
|
207
|
+
rescue => e
|
|
208
|
+
$stderr.puts "LocalFile: TCX parse error: #{e.message}"
|
|
209
|
+
nil
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def extract_fit_points(file_path)
|
|
213
|
+
records = GitFit::FIT::Decoder.decode(file_path)
|
|
214
|
+
records.filter_map do |r|
|
|
215
|
+
lat = r["positionLat"]
|
|
216
|
+
lon = r["positionLong"]
|
|
217
|
+
next unless lat && lon
|
|
218
|
+
ts = r["timestamp"]
|
|
219
|
+
time = ts.is_a?(Numeric) ? (Time.utc(1989, 12, 31) + ts) : (Time.parse(ts.to_s) rescue nil)
|
|
220
|
+
[lat, lon, r["altitude"], time, r["heartRate"]]
|
|
221
|
+
end
|
|
222
|
+
rescue => e
|
|
223
|
+
$stderr.puts "LocalFile: FIT decode error: #{e.message}"
|
|
224
|
+
nil
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def parse_timestamp(str)
|
|
228
|
+
return nil unless str
|
|
229
|
+
Time.parse(str)
|
|
230
|
+
rescue ArgumentError
|
|
231
|
+
nil
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def existing_run_id?(platform_id)
|
|
235
|
+
!@db[:activities].where(run_id: platform_id).empty?
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def utc_to_local(utc_iso8601, tz_name)
|
|
239
|
+
return utc_iso8601 unless utc_iso8601 && tz_name
|
|
240
|
+
t = Time.parse(utc_iso8601)
|
|
241
|
+
return utc_iso8601 unless t.utc?
|
|
242
|
+
tz = TZInfo::Timezone.get(tz_name)
|
|
243
|
+
local = tz.utc_to_local(t)
|
|
244
|
+
local.iso8601
|
|
245
|
+
rescue TZInfo::InvalidTimezoneIdentifier, ArgumentError
|
|
246
|
+
utc_iso8601
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
require "date"
|
|
2
|
+
|
|
3
|
+
module GitFit
|
|
4
|
+
module Source
|
|
5
|
+
class Base
|
|
6
|
+
def initialize(config:, db:, activity_filter: nil, privacy: nil, time_budget: nil)
|
|
7
|
+
@config = config
|
|
8
|
+
@db = db
|
|
9
|
+
@activity_filter = activity_filter
|
|
10
|
+
@privacy = privacy
|
|
11
|
+
@time_budget = time_budget
|
|
12
|
+
@start_time = nil
|
|
13
|
+
@progress_reported = false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
attr_reader :progress_reported
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
return 0 unless before_call
|
|
20
|
+
return 0 unless respond_to?(:authenticate) ? authenticate : true
|
|
21
|
+
ids = pending_ids
|
|
22
|
+
return 0 if ids.nil? || ids.empty?
|
|
23
|
+
total = ids.size
|
|
24
|
+
count = 0
|
|
25
|
+
ids.filter_map do |p|
|
|
26
|
+
r = process_one(p)
|
|
27
|
+
count += 1
|
|
28
|
+
report_progress(count, total, build_progress_info(r)) if r
|
|
29
|
+
r
|
|
30
|
+
end.size
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def before_call
|
|
34
|
+
@start_time = Time.now
|
|
35
|
+
true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def pending_ids
|
|
39
|
+
raise NotImplementedError
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def process_one(pending)
|
|
43
|
+
raise NotImplementedError
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def build_progress_info(attrs)
|
|
47
|
+
parts = []
|
|
48
|
+
sd = attrs["start_date"] || attrs[:start_date]
|
|
49
|
+
if sd
|
|
50
|
+
d = Date.parse(sd.to_s) rescue nil
|
|
51
|
+
if d
|
|
52
|
+
date_str = d.year == Date.today.year ? d.strftime("%m-%d") : d.strftime("%Y-%m-%d")
|
|
53
|
+
parts << date_str
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
type = attrs["sport_category"] || attrs[:sport_category]
|
|
57
|
+
parts << type if type
|
|
58
|
+
dist = attrs["distance"] || attrs[:distance]
|
|
59
|
+
if dist && dist.to_f > 0
|
|
60
|
+
parts << "#{format('%.1f', dist.to_f / 1000)}km"
|
|
61
|
+
end
|
|
62
|
+
source = attrs["source"] || attrs[:source] || source_label
|
|
63
|
+
name = attrs["name"] || attrs[:name]
|
|
64
|
+
display_name = name.to_s.strip
|
|
65
|
+
display_name = "#{source} Activity" if display_name.empty?
|
|
66
|
+
chars = display_name.chars
|
|
67
|
+
display_name = chars.first(30).join + "…" if chars.size > 30
|
|
68
|
+
parts << display_name
|
|
69
|
+
parts.join(" | ") unless parts.empty?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
protected
|
|
73
|
+
|
|
74
|
+
def time_expired?
|
|
75
|
+
return false unless @time_budget.is_a?(Numeric) && @time_budget > 0 && @start_time
|
|
76
|
+
(Time.now - @start_time) > @time_budget
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def raw_dir
|
|
80
|
+
src = self.class.name.split("::").last.downcase
|
|
81
|
+
File.join("data", "raw", src)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def raw_path(platform_id)
|
|
85
|
+
raise NotImplementedError
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def raw_file_exists?(platform_id)
|
|
89
|
+
File.exist?(raw_path(platform_id))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def std_dir
|
|
93
|
+
src = self.class.name.split("::").last.downcase
|
|
94
|
+
File.join("data", "std", src)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def std_path(platform_id, ext = "json")
|
|
98
|
+
File.join(std_dir, "#{platform_id}.#{ext}")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def report_progress(count, total = nil, info = nil)
|
|
102
|
+
return unless show_progress?(count, total)
|
|
103
|
+
@progress_reported = true
|
|
104
|
+
line = "#{source_label}: #{count}"
|
|
105
|
+
line += "/#{total}" if total
|
|
106
|
+
line += " #{info}" if info
|
|
107
|
+
line += " "
|
|
108
|
+
print "\r#{line}"
|
|
109
|
+
$stdout.flush
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def show_progress?(count, total)
|
|
113
|
+
return true if count == 1
|
|
114
|
+
return true if total && (total - count) <= 50
|
|
115
|
+
step = progress_step(total)
|
|
116
|
+
step <= 1 || count % step == 0
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def progress_step(total)
|
|
120
|
+
return 1 unless total && total > 0
|
|
121
|
+
[1, (1.5 * (total ** 0.4)).to_i].max
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def source_label
|
|
125
|
+
name = self.class.name
|
|
126
|
+
name ? name.split("::").last : "Source"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def upsert_activity(attrs)
|
|
130
|
+
attrs = attrs.transform_keys(&:to_s)
|
|
131
|
+
now = Time.now.utc
|
|
132
|
+
|
|
133
|
+
if @privacy && !@privacy.before_saving?
|
|
134
|
+
attrs["summary_polyline"] = @privacy.filter(attrs["summary_polyline"])
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
existing = @db[:activities].where(run_id: attrs["run_id"]).first
|
|
138
|
+
if existing
|
|
139
|
+
attrs.delete("created_at")
|
|
140
|
+
attrs["updated_at"] = now
|
|
141
|
+
@db[:activities].where(run_id: attrs["run_id"]).update(attrs)
|
|
142
|
+
else
|
|
143
|
+
attrs["created_at"] ||= now
|
|
144
|
+
attrs["updated_at"] ||= now
|
|
145
|
+
@db[:activities].insert(attrs)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def skip_type?(sport_type)
|
|
150
|
+
return false unless @activity_filter
|
|
151
|
+
!@activity_filter.include?(sport_type.to_s.downcase)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module GitFit
|
|
2
|
+
module Source
|
|
3
|
+
class Tally
|
|
4
|
+
DIGITS = %w[零 一 二 三 四 五 六 七 八 九 十]
|
|
5
|
+
|
|
6
|
+
def initialize(count = 0)
|
|
7
|
+
@count = count
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def add(n = 1)
|
|
11
|
+
@count += n
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_s
|
|
15
|
+
return @count.to_s if @count > 9999
|
|
16
|
+
chinese_numeral
|
|
17
|
+
rescue StandardError
|
|
18
|
+
@count.to_s
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def chinese_numeral
|
|
24
|
+
n = @count
|
|
25
|
+
return DIGITS[0] if n == 0
|
|
26
|
+
return DIGITS[n] if n <= 10
|
|
27
|
+
|
|
28
|
+
result = ""
|
|
29
|
+
thousands = n / 1000
|
|
30
|
+
n %= 1000
|
|
31
|
+
result += DIGITS[thousands] + "千" if thousands > 0
|
|
32
|
+
|
|
33
|
+
hundreds = n / 100
|
|
34
|
+
n %= 100
|
|
35
|
+
if hundreds > 0
|
|
36
|
+
result += DIGITS[hundreds] + "百"
|
|
37
|
+
elsif thousands > 0 && n > 0
|
|
38
|
+
result += "零"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
tens = n / 10
|
|
42
|
+
ones = n % 10
|
|
43
|
+
if tens > 0
|
|
44
|
+
join_ten = tens > 1 || !result.empty?
|
|
45
|
+
result += (join_ten ? DIGITS[tens] : "") + "十"
|
|
46
|
+
elsif !result.empty? && tens == 0 && ones > 0 && !result.end_with?("零")
|
|
47
|
+
result += "零"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
result += DIGITS[ones] if ones > 0
|
|
51
|
+
result
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/git_fit/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: git-fit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lax
|
|
@@ -93,6 +93,34 @@ dependencies:
|
|
|
93
93
|
- - "~>"
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
95
|
version: '1.5'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: tzinfo
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '2.0'
|
|
103
|
+
type: :runtime
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '2.0'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: rubyzip
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '2.0'
|
|
117
|
+
type: :runtime
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '2.0'
|
|
96
124
|
description: A git extension CLI for aggregating, managing, and exporting fitness
|
|
97
125
|
activity data from multiple sources (Garmin, Strava, Keep, etc.)
|
|
98
126
|
email:
|
|
@@ -105,22 +133,33 @@ files:
|
|
|
105
133
|
- exe/git-fit
|
|
106
134
|
- lib/git-fit.rb
|
|
107
135
|
- lib/git_fit/cli.rb
|
|
136
|
+
- lib/git_fit/cli/export.rb
|
|
137
|
+
- lib/git_fit/cli/import_cli.rb
|
|
108
138
|
- lib/git_fit/config.rb
|
|
109
139
|
- lib/git_fit/config_template.rb
|
|
110
140
|
- lib/git_fit/db/activity.rb
|
|
111
141
|
- lib/git_fit/db/cli.rb
|
|
112
142
|
- lib/git_fit/db/connection.rb
|
|
143
|
+
- lib/git_fit/export.rb
|
|
144
|
+
- lib/git_fit/export/defaults.rb
|
|
145
|
+
- lib/git_fit/export/json.rb
|
|
113
146
|
- lib/git_fit/fit.rb
|
|
114
147
|
- lib/git_fit/fit/decoder.rb
|
|
115
148
|
- lib/git_fit/geo.rb
|
|
116
149
|
- lib/git_fit/geo/coord_transform.rb
|
|
117
150
|
- lib/git_fit/geo/polyline.rb
|
|
151
|
+
- lib/git_fit/import.rb
|
|
152
|
+
- lib/git_fit/import/apple_health.rb
|
|
153
|
+
- lib/git_fit/import/local_file.rb
|
|
118
154
|
- lib/git_fit/parser.rb
|
|
119
155
|
- lib/git_fit/parser/base.rb
|
|
120
156
|
- lib/git_fit/parser/fit.rb
|
|
121
157
|
- lib/git_fit/parser/gpx.rb
|
|
122
158
|
- lib/git_fit/parser/tcx.rb
|
|
123
159
|
- lib/git_fit/privacy/polyline_filter.rb
|
|
160
|
+
- lib/git_fit/source.rb
|
|
161
|
+
- lib/git_fit/source/base.rb
|
|
162
|
+
- lib/git_fit/source/tally.rb
|
|
124
163
|
- lib/git_fit/sport_mapper.rb
|
|
125
164
|
- lib/git_fit/sync/base.rb
|
|
126
165
|
- lib/git_fit/sync/strava.rb
|