git-fit 0.7.9 → 0.8.0

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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/lib/git-fit.rb +44 -42
  3. data/lib/git_fit/cli/export.rb +8 -6
  4. data/lib/git_fit/cli/geo_cli.rb +10 -8
  5. data/lib/git_fit/cli/gh_cli.rb +24 -22
  6. data/lib/git_fit/cli/import_cli.rb +40 -38
  7. data/lib/git_fit/cli/install_cli.rb +10 -8
  8. data/lib/git_fit/cli/sync.rb +9 -7
  9. data/lib/git_fit/cli.rb +29 -27
  10. data/lib/git_fit/config.rb +42 -28
  11. data/lib/git_fit/config_template.rb +2 -0
  12. data/lib/git_fit/db/activity.rb +2 -0
  13. data/lib/git_fit/db/cli.rb +11 -9
  14. data/lib/git_fit/db/connection.rb +8 -6
  15. data/lib/git_fit/db/rebuild.rb +5 -3
  16. data/lib/git_fit/export/defaults.rb +14 -12
  17. data/lib/git_fit/export/json.rb +5 -3
  18. data/lib/git_fit/export.rb +4 -2
  19. data/lib/git_fit/fit/decoder.rb +9 -7
  20. data/lib/git_fit/fit.rb +3 -1
  21. data/lib/git_fit/geo/amap_client.rb +15 -13
  22. data/lib/git_fit/geo/coord_transform.rb +2 -0
  23. data/lib/git_fit/geo/division_detector.rb +31 -26
  24. data/lib/git_fit/geo/nominatim_client.rb +14 -12
  25. data/lib/git_fit/geo/polyline.rb +4 -2
  26. data/lib/git_fit/geo/reverse_geocode.rb +11 -9
  27. data/lib/git_fit/geo.rb +8 -6
  28. data/lib/git_fit/import/apple_health.rb +84 -82
  29. data/lib/git_fit/import/local_file.rb +45 -42
  30. data/lib/git_fit/import.rb +4 -2
  31. data/lib/git_fit/install/actions.rb +15 -13
  32. data/lib/git_fit/parser/base.rb +12 -10
  33. data/lib/git_fit/parser/fit.rb +11 -9
  34. data/lib/git_fit/parser/gpx.rb +17 -15
  35. data/lib/git_fit/parser/tcx.rb +22 -20
  36. data/lib/git_fit/parser.rb +6 -4
  37. data/lib/git_fit/privacy/polyline_filter.rb +6 -4
  38. data/lib/git_fit/source/base.rb +25 -23
  39. data/lib/git_fit/source/tally.rb +9 -7
  40. data/lib/git_fit/source.rb +4 -2
  41. data/lib/git_fit/sport_mapper.rb +23 -21
  42. data/lib/git_fit/sync/base.rb +29 -27
  43. data/lib/git_fit/sync/garmin.rb +5 -3
  44. data/lib/git_fit/sync/garmin_base.rb +145 -143
  45. data/lib/git_fit/sync/garmin_base_di.rb +42 -40
  46. data/lib/git_fit/sync/garmin_cn.rb +8 -6
  47. data/lib/git_fit/sync/igpsport.rb +43 -41
  48. data/lib/git_fit/sync/keep.rb +98 -96
  49. data/lib/git_fit/sync/lorem.rb +31 -29
  50. data/lib/git_fit/sync/runner.rb +14 -11
  51. data/lib/git_fit/sync/strava.rb +61 -52
  52. data/lib/git_fit/sync/xingzhe.rb +52 -50
  53. data/lib/git_fit/sync/xoss.rb +68 -66
  54. data/lib/git_fit/timezone/resolver.rb +5 -3
  55. data/lib/git_fit/util/tally.rb +10 -8
  56. data/lib/git_fit/util.rb +2 -0
  57. data/lib/git_fit/version.rb +3 -1
  58. metadata +1 -1
@@ -1,26 +1,28 @@
1
- require_relative "base"
2
- require "fileutils"
3
- require "tzinfo"
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require 'fileutils'
5
+ require 'tzinfo'
4
6
 
5
7
  module GitFit
6
8
  module Sync
7
9
  class SyncError < RuntimeError; end unless const_defined?(:SyncError)
8
10
 
9
11
  class XOSS < Base
10
- FIT_FILE_PATH = "/api/v3/activity/%<id>s/fit_file/"
11
- BASE_URL = "https://backend.xoss.co"
12
- LOGIN_PATH = "/api/v1/jwt-token/"
13
- REFRESH_PATH = "/api/v1/jwt-token/refresh/"
14
- ACTIVITIES_PATH = "/api/v3/activity/"
12
+ FIT_FILE_PATH = '/api/v3/activity/%<id>s/fit_file/'
13
+ BASE_URL = 'https://backend.xoss.co'
14
+ LOGIN_PATH = '/api/v1/jwt-token/'
15
+ REFRESH_PATH = '/api/v1/jwt-token/refresh/'
16
+ ACTIVITIES_PATH = '/api/v3/activity/'
15
17
  ACCESS_TOKEN_TTL = 86400
16
18
 
17
19
  SPORT_MAP = {
18
- 1 => "run",
19
- 2 => "ride",
20
- 3 => "other",
21
- 4 => "workout",
22
- 5 => "swim",
23
- 11 => "walk"
20
+ 1 => 'run',
21
+ 2 => 'ride',
22
+ 3 => 'other',
23
+ 4 => 'workout',
24
+ 5 => 'swim',
25
+ 11 => 'walk',
24
26
  }.freeze
25
27
 
26
28
  register_adapter
@@ -28,9 +30,9 @@ module GitFit
28
30
 
29
31
  def initialize(...)
30
32
  super
31
- @email = @config["email"]
32
- @password = @config["password"]
33
- @token_path = @config["token_path"] || File.join("data", "cache", "xoss_tokens.json")
33
+ @email = @config['email']
34
+ @password = @config['password']
35
+ @token_path = @config['token_path'] || File.join('data', 'cache', 'xoss_tokens.json')
34
36
  end
35
37
 
36
38
  def before_call
@@ -54,9 +56,9 @@ module GitFit
54
56
  acts = fetch_page(offset, limit)
55
57
  break if acts.nil? || acts.empty?
56
58
  acts.each do |act|
57
- act_id = act["id"].to_s
59
+ act_id = act['id'].to_s
58
60
  next if existing.include?(act_id) && raw_file_exists?(act_id)
59
- type = map_sport(act["sport"])
61
+ type = map_sport(act['sport'])
60
62
  next if skip_type?(type)
61
63
  ids << { id: act_id, type: type, summary: act }
62
64
  end
@@ -96,7 +98,7 @@ module GitFit
96
98
  end
97
99
 
98
100
  def proxy_uri
99
- uri = URI(ENV["http_proxy"] || ENV["HTTP_PROXY"] || ENV["https_proxy"] || ENV["HTTPS_PROXY"] || "")
101
+ uri = URI(ENV['http_proxy'] || ENV['HTTP_PROXY'] || ENV['https_proxy'] || ENV['HTTPS_PROXY'] || '')
100
102
  uri.host ? uri : nil
101
103
  end
102
104
 
@@ -104,21 +106,21 @@ module GitFit
104
106
  retries ||= 0
105
107
  resp = http_request(:post, LOGIN_PATH,
106
108
  body: JSON.generate({ email: @email, password: @password }),
107
- headers: { "Content-Type" => "application/json", "Origin" => "xoss" }
109
+ headers: { 'Content-Type' => 'application/json', 'Origin' => 'xoss' }
108
110
  )
109
111
  return false unless resp.code.to_i == 200
110
112
 
111
113
  data = JSON.parse(resp.body)
112
- return false unless data["code"] == 0
114
+ return false unless data['code'] == 0
113
115
 
114
- @access_token = data.dig("data", "access")
115
- @refresh_token = data.dig("data", "refresh")
116
+ @access_token = data.dig('data', 'access')
117
+ @refresh_token = data.dig('data', 'refresh')
116
118
  unless @access_token
117
119
  remove_stale_tokens
118
120
  return false
119
121
  end
120
122
 
121
- @auth_headers = { "Authorization" => "Bearer #{@access_token}", "Origin" => "xoss" }
123
+ @auth_headers = { 'Authorization' => "Bearer #{@access_token}", 'Origin' => 'xoss' }
122
124
  save_tokens
123
125
  true
124
126
  rescue Net::OpenTimeout, Net::ReadTimeout => e
@@ -137,12 +139,12 @@ module GitFit
137
139
  return false unless File.exist?(token_path)
138
140
 
139
141
  data = JSON.parse(File.read(token_path))
140
- @access_token = data["access"]
141
- @refresh_token = data["refresh"]
142
- @expires_at = data["expires_at"]
143
- @auth_headers = { "Authorization" => "Bearer #{@access_token}", "Origin" => "xoss" } if @access_token
142
+ @access_token = data['access']
143
+ @refresh_token = data['refresh']
144
+ @expires_at = data['expires_at']
145
+ @auth_headers = { 'Authorization' => "Bearer #{@access_token}", 'Origin' => 'xoss' } if @access_token
144
146
  true
145
- rescue => e
147
+ rescue StandardError => e
146
148
  warn "XOSS: failed to load cached tokens: #{e.message}"
147
149
  false
148
150
  end
@@ -154,15 +156,15 @@ module GitFit
154
156
  File.write(token_path, JSON.generate({
155
157
  access: @access_token,
156
158
  refresh: @refresh_token,
157
- expires_at: @expires_at || Time.now.to_i + ACCESS_TOKEN_TTL
159
+ expires_at: @expires_at || Time.now.to_i + ACCESS_TOKEN_TTL,
158
160
  }))
159
- rescue => e
161
+ rescue StandardError => e
160
162
  warn "XOSS: failed to save tokens: #{e.message}"
161
163
  end
162
164
 
163
165
  def remove_stale_tokens
164
166
  File.delete(token_path) if File.exist?(token_path)
165
- rescue => e
167
+ rescue StandardError => e
166
168
  warn "XOSS: failed to remove stale tokens: #{e.message}"
167
169
  end
168
170
 
@@ -178,7 +180,7 @@ module GitFit
178
180
  retries ||= 0
179
181
  resp = http_request(:post, REFRESH_PATH,
180
182
  body: JSON.generate({ refresh: @refresh_token }),
181
- headers: { "Content-Type" => "application/json", "Origin" => "xoss" }
183
+ headers: { 'Content-Type' => 'application/json', 'Origin' => 'xoss' }
182
184
  )
183
185
  unless resp.code.to_i == 200
184
186
  remove_stale_tokens
@@ -186,17 +188,17 @@ module GitFit
186
188
  end
187
189
 
188
190
  data = JSON.parse(resp.body)
189
- unless data["code"] == 0
191
+ unless data['code'] == 0
190
192
  remove_stale_tokens
191
193
  return false
192
194
  end
193
195
 
194
- @access_token = data.dig("data", "access")
195
- @refresh_token = data.dig("data", "refresh")
196
+ @access_token = data.dig('data', 'access')
197
+ @refresh_token = data.dig('data', 'refresh')
196
198
  return false unless @access_token
197
199
 
198
200
  @expires_at = Time.now.to_i + ACCESS_TOKEN_TTL
199
- @auth_headers = { "Authorization" => "Bearer #{@access_token}", "Origin" => "xoss" }
201
+ @auth_headers = { 'Authorization' => "Bearer #{@access_token}", 'Origin' => 'xoss' }
200
202
  save_tokens
201
203
  true
202
204
  rescue Net::OpenTimeout, Net::ReadTimeout => e
@@ -223,7 +225,7 @@ module GitFit
223
225
  )
224
226
  return nil unless resp.code.to_i == 200
225
227
 
226
- JSON.parse(resp.body).dig("data", "results")
228
+ JSON.parse(resp.body).dig('data', 'results')
227
229
  rescue Net::OpenTimeout, Net::ReadTimeout => e
228
230
  if (retries += 1) <= 2
229
231
  sleep 2
@@ -245,60 +247,60 @@ module GitFit
245
247
  def resolve_local_time(start_utc, wgs_points)
246
248
  return start_utc.iso8601 unless wgs_points&.any?
247
249
 
248
- lat = wgs_points.first["positionLat"]
249
- lng = wgs_points.first["positionLong"]
250
+ lat = wgs_points.first['positionLat']
251
+ lng = wgs_points.first['positionLong']
250
252
  return start_utc.iso8601 unless lat && lng
251
253
 
252
254
  tz_string = tz_resolver.resolve(lat, lng)
253
255
  tz = TZInfo::Timezone.get(tz_string)
254
256
  tz.utc_to_local(start_utc).iso8601
255
- rescue => e
257
+ rescue StandardError => e
256
258
  warn "XOSS: timezone resolution failed: #{e.message}"
257
259
  start_utc.iso8601
258
260
  end
259
261
 
260
262
  def process_activity(act)
261
- act_id = act["id"].to_s
262
- type = map_sport(act["sport"])
263
+ act_id = act['id'].to_s
264
+ type = map_sport(act['sport'])
263
265
 
264
- start_t = Time.at(act["start_timestamp"]).utc rescue nil
266
+ start_t = Time.at(act['start_timestamp']).utc rescue nil
265
267
  return false unless start_t
266
268
 
267
- distance = act["distance"].to_f
268
- moving_time = act["duration"].to_i
269
+ distance = act['distance'].to_f
270
+ moving_time = act['duration'].to_i
269
271
  avg_speed = distance > 0 && moving_time > 0 ? (distance / moving_time).round(2) : nil
270
272
 
271
273
  attrs = {
272
274
  run_id: "xoss_#{act_id}",
273
- name: act["title"] || "XOSS Activity",
275
+ name: act['title'] || 'XOSS Activity',
274
276
  distance: distance,
275
277
  moving_time: moving_time > 0 ? moving_time : nil,
276
278
  elapsed_time: moving_time > 0 ? moving_time : nil,
277
279
  sport_category: type,
278
- sport_type: act["sub_sport"].to_s,
280
+ sport_type: act['sub_sport'].to_s,
279
281
  start_date: start_t.iso8601,
280
282
  start_date_local: start_t.iso8601,
281
283
  location_country: nil,
282
284
  average_speed: avg_speed,
283
- elevation_gain: act["elevation_gain"].to_f.nonzero?,
284
- calories: act["total_calories"]&.to_i&.nonzero?,
285
- source: "xoss"
285
+ elevation_gain: act['elevation_gain'].to_f.nonzero?,
286
+ calories: act['total_calories']&.to_i&.nonzero?,
287
+ source: 'xoss',
286
288
  }
287
289
 
288
290
  wgs_points = []
289
- if act["fit_file_hash"].to_s != ""
291
+ if act['fit_file_hash'].to_s != ''
290
292
  fit_data = download_fit(act_id)
291
293
  if fit_data
292
- write_source_archive(fit_data, act_id, "fit")
294
+ write_source_archive(fit_data, act_id, 'fit')
293
295
  begin
294
296
  wgs_points = FIT::Decoder.decode(raw_path(act_id))
295
- rescue => e
297
+ rescue StandardError => e
296
298
  warn "XOSS: FIT decode failed for #{act_id}: #{e.message}"
297
299
  wgs_points = []
298
300
  end
299
301
  if wgs_points.any?
300
302
  write_std(wgs_points, act_id)
301
- pts = wgs_points.map { |pt| [pt["positionLat"], pt["positionLong"]] }
303
+ pts = wgs_points.map { |pt| [pt['positionLat'], pt['positionLong']] }
302
304
  attrs[:summary_polyline] = Geo::Polyline.encode(pts)
303
305
  attrs.merge!(compute_sensor_summary(wgs_points))
304
306
  end
@@ -315,9 +317,9 @@ module GitFit
315
317
  resp = http_request(:get, format(FIT_FILE_PATH, id: act_id), headers: @auth_headers)
316
318
  return nil unless resp.code.to_i == 302
317
319
 
318
- fit_uri = URI(resp["location"])
320
+ fit_uri = URI(resp['location'])
319
321
  fit_http = Net::HTTP.new(fit_uri.host, fit_uri.port)
320
- fit_http.use_ssl = fit_uri.scheme == "https"
322
+ fit_http.use_ssl = fit_uri.scheme == 'https'
321
323
  fit_http.open_timeout = 60
322
324
  fit_http.read_timeout = 180
323
325
  fit_resp = fit_http.start { |h| h.request(Net::HTTP::Get.new(fit_uri)) }
@@ -341,10 +343,10 @@ module GitFit
341
343
  def compute_sensor_summary(records)
342
344
  return {} if records.empty?
343
345
 
344
- cad = records.map { |r| r["cadence"] }.compact
345
- pow = records.map { |r| r["power"] }.compact
346
- tmp = records.map { |r| r["temperature"] }.compact
347
- hr = records.map { |r| r["heart_rate"] }.compact
346
+ cad = records.map { |r| r['cadence'] }.compact
347
+ pow = records.map { |r| r['power'] }.compact
348
+ tmp = records.map { |r| r['temperature'] }.compact
349
+ hr = records.map { |r| r['heart_rate'] }.compact
348
350
 
349
351
  {
350
352
  average_cadence: cad.any? ? (cad.sum.to_f / cad.size).round(1) : nil,
@@ -352,17 +354,17 @@ module GitFit
352
354
  average_power: pow.any? ? (pow.sum.to_f / pow.size).round(1) : nil,
353
355
  max_power: pow.max,
354
356
  average_temperature: tmp.any? ? (tmp.sum.to_f / tmp.size).round(1) : nil,
355
- average_heartrate: hr.any? ? (hr.sum.to_f / hr.size).round(1) : nil
357
+ average_heartrate: hr.any? ? (hr.sum.to_f / hr.size).round(1) : nil,
356
358
  }
357
359
  end
358
360
 
359
361
  def map_sport(sport)
360
- SPORT_MAP[sport] || "other"
362
+ SPORT_MAP[sport] || 'other'
361
363
  end
362
364
 
363
365
  def existing_run_ids
364
- @db[:activities].where(source: "xoss").select_map(:run_id)
365
- .map { |id| id.sub("xoss_", "") }
366
+ @db[:activities].where(source: 'xoss').select_map(:run_id)
367
+ .map { |id| id.sub('xoss_', '') }
366
368
  end
367
369
  end
368
370
  end
@@ -1,12 +1,14 @@
1
- require "timezone_finder"
1
+ # frozen_string_literal: true
2
+
3
+ require 'timezone_finder'
2
4
 
3
5
  module GitFit
4
6
  module Timezone
5
7
  class Resolver
6
8
  def initialize(config, finder: nil)
7
- default = config.is_a?(Hash) ? config.dig("system", "timezone") : config
9
+ default = config.is_a?(Hash) ? config.dig('system', 'timezone') : config
8
10
  @finder = finder || TimezoneFinder.create
9
- @default = default || "Asia/Shanghai"
11
+ @default = default || 'Asia/Shanghai'
10
12
  end
11
13
 
12
14
  def resolve(lat, lng)
@@ -1,4 +1,6 @@
1
- require "json"
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
2
4
 
3
5
  module GitFit
4
6
  module Util
@@ -62,26 +64,26 @@ module GitFit
62
64
  return DIGITS[0] if n == 0
63
65
  return DIGITS[n] if n <= 10
64
66
 
65
- result = ""
67
+ result = ''
66
68
  thousands = n / 1000
67
69
  n %= 1000
68
- result += DIGITS[thousands] + "" if thousands > 0
70
+ result += DIGITS[thousands] + '' if thousands > 0
69
71
 
70
72
  hundreds = n / 100
71
73
  n %= 100
72
74
  if hundreds > 0
73
- result += DIGITS[hundreds] + ""
75
+ result += DIGITS[hundreds] + ''
74
76
  elsif thousands > 0 && n > 0
75
- result += ""
77
+ result += ''
76
78
  end
77
79
 
78
80
  tens = n / 10
79
81
  ones = n % 10
80
82
  if tens > 0
81
83
  join_ten = tens > 1 || !result.empty?
82
- result += (join_ten ? DIGITS[tens] : "") + ""
83
- elsif !result.empty? && tens == 0 && ones > 0 && !result.end_with?("")
84
- result += ""
84
+ result += (join_ten ? DIGITS[tens] : '') + ''
85
+ elsif !result.empty? && tens == 0 && ones > 0 && !result.end_with?('')
86
+ result += ''
85
87
  end
86
88
 
87
89
  result += DIGITS[ones] if ones > 0
data/lib/git_fit/util.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module GitFit
2
4
  module Util
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module GitFit
2
- VERSION = "0.7.9"
4
+ VERSION = '0.8.0'
3
5
  end
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.7.9
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lax