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.
- checksums.yaml +4 -4
- data/lib/git-fit.rb +44 -42
- data/lib/git_fit/cli/export.rb +8 -6
- data/lib/git_fit/cli/geo_cli.rb +10 -8
- data/lib/git_fit/cli/gh_cli.rb +24 -22
- data/lib/git_fit/cli/import_cli.rb +40 -38
- data/lib/git_fit/cli/install_cli.rb +10 -8
- data/lib/git_fit/cli/sync.rb +9 -7
- data/lib/git_fit/cli.rb +29 -27
- data/lib/git_fit/config.rb +42 -28
- data/lib/git_fit/config_template.rb +2 -0
- data/lib/git_fit/db/activity.rb +2 -0
- data/lib/git_fit/db/cli.rb +11 -9
- data/lib/git_fit/db/connection.rb +8 -6
- data/lib/git_fit/db/rebuild.rb +5 -3
- data/lib/git_fit/export/defaults.rb +14 -12
- data/lib/git_fit/export/json.rb +5 -3
- data/lib/git_fit/export.rb +4 -2
- data/lib/git_fit/fit/decoder.rb +9 -7
- data/lib/git_fit/fit.rb +3 -1
- data/lib/git_fit/geo/amap_client.rb +15 -13
- data/lib/git_fit/geo/coord_transform.rb +2 -0
- data/lib/git_fit/geo/division_detector.rb +31 -26
- data/lib/git_fit/geo/nominatim_client.rb +14 -12
- data/lib/git_fit/geo/polyline.rb +4 -2
- data/lib/git_fit/geo/reverse_geocode.rb +11 -9
- data/lib/git_fit/geo.rb +8 -6
- data/lib/git_fit/import/apple_health.rb +84 -82
- data/lib/git_fit/import/local_file.rb +45 -42
- data/lib/git_fit/import.rb +4 -2
- data/lib/git_fit/install/actions.rb +15 -13
- data/lib/git_fit/parser/base.rb +12 -10
- data/lib/git_fit/parser/fit.rb +11 -9
- data/lib/git_fit/parser/gpx.rb +17 -15
- data/lib/git_fit/parser/tcx.rb +22 -20
- data/lib/git_fit/parser.rb +6 -4
- data/lib/git_fit/privacy/polyline_filter.rb +6 -4
- data/lib/git_fit/source/base.rb +25 -23
- data/lib/git_fit/source/tally.rb +9 -7
- data/lib/git_fit/source.rb +4 -2
- data/lib/git_fit/sport_mapper.rb +23 -21
- data/lib/git_fit/sync/base.rb +29 -27
- data/lib/git_fit/sync/garmin.rb +5 -3
- data/lib/git_fit/sync/garmin_base.rb +145 -143
- data/lib/git_fit/sync/garmin_base_di.rb +42 -40
- data/lib/git_fit/sync/garmin_cn.rb +8 -6
- data/lib/git_fit/sync/igpsport.rb +43 -41
- data/lib/git_fit/sync/keep.rb +98 -96
- data/lib/git_fit/sync/lorem.rb +31 -29
- data/lib/git_fit/sync/runner.rb +14 -11
- data/lib/git_fit/sync/strava.rb +61 -52
- data/lib/git_fit/sync/xingzhe.rb +52 -50
- data/lib/git_fit/sync/xoss.rb +68 -66
- data/lib/git_fit/timezone/resolver.rb +5 -3
- data/lib/git_fit/util/tally.rb +10 -8
- data/lib/git_fit/util.rb +2 -0
- data/lib/git_fit/version.rb +3 -1
- metadata +1 -1
data/lib/git_fit/sync/xoss.rb
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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 =
|
|
11
|
-
BASE_URL =
|
|
12
|
-
LOGIN_PATH =
|
|
13
|
-
REFRESH_PATH =
|
|
14
|
-
ACTIVITIES_PATH =
|
|
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 =>
|
|
19
|
-
2 =>
|
|
20
|
-
3 =>
|
|
21
|
-
4 =>
|
|
22
|
-
5 =>
|
|
23
|
-
11 =>
|
|
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[
|
|
32
|
-
@password = @config[
|
|
33
|
-
@token_path = @config[
|
|
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[
|
|
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[
|
|
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[
|
|
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: {
|
|
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[
|
|
114
|
+
return false unless data['code'] == 0
|
|
113
115
|
|
|
114
|
-
@access_token = data.dig(
|
|
115
|
-
@refresh_token = data.dig(
|
|
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 = {
|
|
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[
|
|
141
|
-
@refresh_token = data[
|
|
142
|
-
@expires_at = data[
|
|
143
|
-
@auth_headers = {
|
|
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: {
|
|
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[
|
|
191
|
+
unless data['code'] == 0
|
|
190
192
|
remove_stale_tokens
|
|
191
193
|
return false
|
|
192
194
|
end
|
|
193
195
|
|
|
194
|
-
@access_token = data.dig(
|
|
195
|
-
@refresh_token = data.dig(
|
|
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 = {
|
|
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(
|
|
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[
|
|
249
|
-
lng = wgs_points.first[
|
|
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[
|
|
262
|
-
type = map_sport(act[
|
|
263
|
+
act_id = act['id'].to_s
|
|
264
|
+
type = map_sport(act['sport'])
|
|
263
265
|
|
|
264
|
-
start_t = Time.at(act[
|
|
266
|
+
start_t = Time.at(act['start_timestamp']).utc rescue nil
|
|
265
267
|
return false unless start_t
|
|
266
268
|
|
|
267
|
-
distance = act[
|
|
268
|
-
moving_time = act[
|
|
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[
|
|
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[
|
|
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[
|
|
284
|
-
calories: act[
|
|
285
|
-
source:
|
|
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[
|
|
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,
|
|
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[
|
|
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[
|
|
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 ==
|
|
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[
|
|
345
|
-
pow = records.map { |r| r[
|
|
346
|
-
tmp = records.map { |r| r[
|
|
347
|
-
hr = records.map { |r| r[
|
|
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] ||
|
|
362
|
+
SPORT_MAP[sport] || 'other'
|
|
361
363
|
end
|
|
362
364
|
|
|
363
365
|
def existing_run_ids
|
|
364
|
-
@db[:activities].where(source:
|
|
365
|
-
.map { |id| id.sub(
|
|
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
|
-
|
|
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(
|
|
9
|
+
default = config.is_a?(Hash) ? config.dig('system', 'timezone') : config
|
|
8
10
|
@finder = finder || TimezoneFinder.create
|
|
9
|
-
@default = default ||
|
|
11
|
+
@default = default || 'Asia/Shanghai'
|
|
10
12
|
end
|
|
11
13
|
|
|
12
14
|
def resolve(lat, lng)
|
data/lib/git_fit/util/tally.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
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] +
|
|
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
data/lib/git_fit/version.rb
CHANGED