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/keep.rb
CHANGED
|
@@ -1,39 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require 'base64'
|
|
5
|
+
require 'zlib'
|
|
6
|
+
require 'openssl'
|
|
7
|
+
require 'time'
|
|
8
|
+
require 'tzinfo'
|
|
7
9
|
|
|
8
10
|
module GitFit
|
|
9
11
|
module Sync
|
|
10
12
|
class Keep < Base
|
|
11
|
-
LOGIN_URL =
|
|
12
|
-
STATS_URL =
|
|
13
|
-
LOG_URL =
|
|
13
|
+
LOGIN_URL = 'https://api.gotokeep.com/v1.1/users/login'
|
|
14
|
+
STATS_URL = 'https://api.gotokeep.com/pd/v3/stats/detail?dateUnit=all&type=%{sport}&lastDate=%{last_date}'
|
|
15
|
+
LOG_URL = 'https://api.gotokeep.com/pd/v3/%{sport}log/%{run_id}'
|
|
14
16
|
|
|
15
|
-
AES_KEY = Base64.decode64(
|
|
16
|
-
AES_IV = Base64.decode64(
|
|
17
|
+
AES_KEY = Base64.decode64('NTZmZTU5OzgyZzpkODczYw==')
|
|
18
|
+
AES_IV = Base64.decode64('MjM0Njg5MjQzMjkyMDMwMA==')
|
|
17
19
|
|
|
18
|
-
PLACEHOLDER_POLYLINE =
|
|
20
|
+
PLACEHOLDER_POLYLINE = 'gqqrFurkeU??'
|
|
19
21
|
|
|
20
22
|
KEEP_TYPES = %w[running hiking cycling].freeze
|
|
21
|
-
KEEP_RAW_EXT =
|
|
23
|
+
KEEP_RAW_EXT = 'json'
|
|
22
24
|
|
|
23
25
|
TYPE_MAP = {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
'outdoorWalking' => 'walk',
|
|
27
|
+
'outdoorRunning' => 'run',
|
|
28
|
+
'outdoorCycling' => 'ride',
|
|
29
|
+
'indoorRunning' => 'run',
|
|
30
|
+
'mountaineering' => 'hike',
|
|
29
31
|
}.freeze
|
|
30
32
|
|
|
31
33
|
STRAVA_TYPE_MAP = {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
'outdoorWalking' => 'Walk',
|
|
35
|
+
'outdoorRunning' => 'Run',
|
|
36
|
+
'outdoorCycling' => 'Ride',
|
|
37
|
+
'indoorRunning' => 'VirtualRun',
|
|
38
|
+
'mountaineering' => 'Hiking',
|
|
37
39
|
}.freeze
|
|
38
40
|
|
|
39
41
|
TIMESTAMP_THRESHOLD = 3_600_000
|
|
@@ -44,11 +46,11 @@ module GitFit
|
|
|
44
46
|
|
|
45
47
|
def initialize(config:, db:, activity_filter: nil, privacy: nil, time_budget: nil)
|
|
46
48
|
super
|
|
47
|
-
@phone = @config[
|
|
48
|
-
@password = @config[
|
|
49
|
+
@phone = @config['phone'] || @config['mobile']
|
|
50
|
+
@password = @config['password']
|
|
49
51
|
@headers = {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
'User-Agent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0',
|
|
53
|
+
'Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8',
|
|
52
54
|
}
|
|
53
55
|
@session = nil
|
|
54
56
|
end
|
|
@@ -96,7 +98,7 @@ module GitFit
|
|
|
96
98
|
headers.each { |k, v| req[k] = v }
|
|
97
99
|
req.body = body.is_a?(Hash) ? URI.encode_www_form(body) : body
|
|
98
100
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
99
|
-
http.use_ssl = uri.scheme ==
|
|
101
|
+
http.use_ssl = uri.scheme == 'https'
|
|
100
102
|
http.open_timeout = 30
|
|
101
103
|
http.read_timeout = 60
|
|
102
104
|
http.start { |h| h.request(req) }
|
|
@@ -107,7 +109,7 @@ module GitFit
|
|
|
107
109
|
req = Net::HTTP::Get.new(uri)
|
|
108
110
|
headers.each { |k, v| req[k] = v }
|
|
109
111
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
110
|
-
http.use_ssl = uri.scheme ==
|
|
112
|
+
http.use_ssl = uri.scheme == 'https'
|
|
111
113
|
http.open_timeout = 30
|
|
112
114
|
http.read_timeout = 60
|
|
113
115
|
http.start { |h| h.request(req) }
|
|
@@ -118,10 +120,10 @@ module GitFit
|
|
|
118
120
|
return nil unless resp.code.to_i == 200
|
|
119
121
|
|
|
120
122
|
data = JSON.parse(resp.body)
|
|
121
|
-
token = data.dig(
|
|
123
|
+
token = data.dig('data', 'token')
|
|
122
124
|
return nil unless token
|
|
123
125
|
|
|
124
|
-
@headers[
|
|
126
|
+
@headers['Authorization'] = "Bearer #{token}"
|
|
125
127
|
@session = true
|
|
126
128
|
rescue JSON::ParserError
|
|
127
129
|
nil
|
|
@@ -134,7 +136,7 @@ module GitFit
|
|
|
134
136
|
end
|
|
135
137
|
|
|
136
138
|
def existing_run_ids
|
|
137
|
-
@db[:activities].where(source:
|
|
139
|
+
@db[:activities].where(source: 'keep').select_map(:run_id).map(&:to_s)
|
|
138
140
|
end
|
|
139
141
|
|
|
140
142
|
def fetch_run_ids(sport)
|
|
@@ -146,19 +148,19 @@ module GitFit
|
|
|
146
148
|
resp = @session ? http_get(url, @headers) : nil
|
|
147
149
|
break unless resp&.code&.to_i == 200
|
|
148
150
|
|
|
149
|
-
data = JSON.parse(resp.body)[
|
|
151
|
+
data = JSON.parse(resp.body)['data']
|
|
150
152
|
break unless data
|
|
151
153
|
|
|
152
|
-
records = data[
|
|
154
|
+
records = data['records'] || []
|
|
153
155
|
records.each do |r|
|
|
154
|
-
logs = r[
|
|
156
|
+
logs = r['logs'] || []
|
|
155
157
|
logs.each do |log|
|
|
156
|
-
stats = log[
|
|
157
|
-
ids << stats[
|
|
158
|
+
stats = log['stats'] || log
|
|
159
|
+
ids << stats['id'] unless stats['isDoubtful']
|
|
158
160
|
end
|
|
159
161
|
end
|
|
160
162
|
|
|
161
|
-
last_date = data[
|
|
163
|
+
last_date = data['lastTimestamp']
|
|
162
164
|
break if last_date.nil? || last_date.zero?
|
|
163
165
|
|
|
164
166
|
spider_sleep
|
|
@@ -175,26 +177,26 @@ module GitFit
|
|
|
175
177
|
return false unless resp.code.to_i == 200
|
|
176
178
|
|
|
177
179
|
data = JSON.parse(resp.body)
|
|
178
|
-
run_data = data[
|
|
180
|
+
run_data = data['data']
|
|
179
181
|
return false unless run_data
|
|
180
182
|
|
|
181
183
|
payload = decrypt_raw_payload(run_data)
|
|
182
|
-
keep_id = extract_id(run_data[
|
|
184
|
+
keep_id = extract_id(run_data['id'])
|
|
183
185
|
return false unless keep_id
|
|
184
186
|
|
|
185
187
|
write_source_archive({
|
|
186
|
-
source:
|
|
187
|
-
decodedAt: Time.now.utc.strftime(
|
|
188
|
+
source: 'keep',
|
|
189
|
+
decodedAt: Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
188
190
|
meta: {
|
|
189
|
-
startTime: run_data[
|
|
190
|
-
endTime: run_data[
|
|
191
|
-
duration: run_data[
|
|
192
|
-
dataType: run_data[
|
|
193
|
-
distance: run_data[
|
|
194
|
-
region: run_data[
|
|
191
|
+
startTime: run_data['startTime'],
|
|
192
|
+
endTime: run_data['endTime'],
|
|
193
|
+
duration: run_data['duration'],
|
|
194
|
+
dataType: run_data['dataType'],
|
|
195
|
+
distance: run_data['distance'],
|
|
196
|
+
region: run_data['region'],
|
|
195
197
|
},
|
|
196
198
|
geoPoints: payload[:geo_points],
|
|
197
|
-
heartRates: payload[:hr_data]
|
|
199
|
+
heartRates: payload[:hr_data],
|
|
198
200
|
}, keep_id)
|
|
199
201
|
|
|
200
202
|
wgs_points = compute_wgs_points(payload, run_data)
|
|
@@ -212,14 +214,14 @@ module GitFit
|
|
|
212
214
|
def upsert_from_raw(compound_id)
|
|
213
215
|
keep_id = extract_id(compound_id) || compound_id
|
|
214
216
|
raw = JSON.parse(File.read(raw_path(keep_id)))
|
|
215
|
-
run_data = raw[
|
|
217
|
+
run_data = raw['meta'].merge('id' => compound_id)
|
|
216
218
|
|
|
217
219
|
wgs = load_wgs_points(keep_id, raw)
|
|
218
220
|
return false unless wgs&.any?
|
|
219
221
|
|
|
220
|
-
if raw[
|
|
221
|
-
vals = raw[
|
|
222
|
-
run_data[
|
|
222
|
+
if raw['heartRates']&.any?
|
|
223
|
+
vals = raw['heartRates'].map { |h| h['value'] }.compact
|
|
224
|
+
run_data['heartRate'] = { 'averageHeartRate' => vals.sum / vals.size } if vals.any?
|
|
223
225
|
end
|
|
224
226
|
|
|
225
227
|
attrs = build_activity_from_wgs(run_data, wgs)
|
|
@@ -233,19 +235,19 @@ module GitFit
|
|
|
233
235
|
std_path = File.join(std_dir, "#{keep_id}.json")
|
|
234
236
|
if File.exist?(std_path)
|
|
235
237
|
pts = JSON.parse(File.read(std_path))
|
|
236
|
-
pts.map { |pt| [pt[
|
|
237
|
-
pt[
|
|
238
|
+
pts.map { |pt| [pt['latitude'], pt['longitude'], pt['altitude'],
|
|
239
|
+
pt['timestamp'] ? Time.parse(pt['timestamp']) : nil, pt['heartrate']] }
|
|
238
240
|
else
|
|
239
|
-
payload = { geo_points: raw[
|
|
240
|
-
run_data = raw[
|
|
241
|
+
payload = { geo_points: raw['geoPoints'], hr_data: raw['heartRates'] }
|
|
242
|
+
run_data = raw['meta']
|
|
241
243
|
compute_wgs_points(payload, run_data)
|
|
242
244
|
end
|
|
243
245
|
end
|
|
244
246
|
|
|
245
247
|
def decrypt_raw_payload(run_data)
|
|
246
248
|
{
|
|
247
|
-
geo_points: decode_geo_points(run_data[
|
|
248
|
-
hr_data: decode_hr_data(run_data.dig(
|
|
249
|
+
geo_points: decode_geo_points(run_data['geoPoints']),
|
|
250
|
+
hr_data: decode_hr_data(run_data.dig('heartRate', 'heartRates')),
|
|
249
251
|
}
|
|
250
252
|
end
|
|
251
253
|
|
|
@@ -254,16 +256,16 @@ module GitFit
|
|
|
254
256
|
hr_data = payload[:hr_data]
|
|
255
257
|
return [] unless geo_points&.any?
|
|
256
258
|
|
|
257
|
-
start_time = run_data[
|
|
258
|
-
use_absolute_ts = geo_points.first[
|
|
259
|
+
start_time = run_data['startTime']
|
|
260
|
+
use_absolute_ts = geo_points.first['timestamp'].to_i > TIMESTAMP_THRESHOLD
|
|
259
261
|
effective_start = use_absolute_ts ? 0 : start_time
|
|
260
262
|
|
|
261
263
|
geo_points.map do |p|
|
|
262
|
-
lat, lng = Geo::CoordTransform.gcj02_to_wgs84_exact(p[
|
|
263
|
-
ts = p[
|
|
264
|
+
lat, lng = Geo::CoordTransform.gcj02_to_wgs84_exact(p['latitude'], p['longitude'])
|
|
265
|
+
ts = p['timestamp'] || p['unixTimestamp']
|
|
264
266
|
pt_time = ts ? Time.at(effective_start / 1000.0 + ts.to_i / 10.0).utc : nil
|
|
265
267
|
hr = find_nearest_hr(hr_data, ts, start_time)
|
|
266
|
-
[lat, lng, p[
|
|
268
|
+
[lat, lng, p['altitude'], pt_time, hr]
|
|
267
269
|
end
|
|
268
270
|
end
|
|
269
271
|
|
|
@@ -278,29 +280,29 @@ module GitFit
|
|
|
278
280
|
tz_string = tz_resolver.resolve(lat, lng)
|
|
279
281
|
tz = TZInfo::Timezone.get(tz_string)
|
|
280
282
|
tz.utc_to_local(start_utc).iso8601
|
|
281
|
-
rescue => e
|
|
283
|
+
rescue StandardError => e
|
|
282
284
|
warn "Keep: timezone resolution failed: #{e.message}"
|
|
283
285
|
start_utc.iso8601
|
|
284
286
|
end
|
|
285
287
|
|
|
286
288
|
def build_activity_from_wgs(run_data, wgs_points)
|
|
287
|
-
keep_id = extract_id(run_data[
|
|
289
|
+
keep_id = extract_id(run_data['id'])
|
|
288
290
|
return nil unless keep_id
|
|
289
291
|
|
|
290
|
-
start_time = run_data[
|
|
291
|
-
end_time = run_data[
|
|
292
|
-
duration = run_data[
|
|
292
|
+
start_time = run_data['startTime']
|
|
293
|
+
end_time = run_data['endTime']
|
|
294
|
+
duration = run_data['duration']
|
|
293
295
|
return nil unless duration
|
|
294
296
|
|
|
295
297
|
start_date = Time.at(start_time / 1000.0).utc
|
|
296
|
-
data_type = run_data[
|
|
298
|
+
data_type = run_data['dataType']
|
|
297
299
|
|
|
298
|
-
avg_hr = run_data.dig(
|
|
300
|
+
avg_hr = run_data.dig('heartRate', 'averageHeartRate')
|
|
299
301
|
avg_hr = nil if avg_hr && avg_hr < 0
|
|
300
302
|
|
|
301
303
|
polyline_str = nil
|
|
302
|
-
total_elevation = run_data[
|
|
303
|
-
computed_distance = run_data[
|
|
304
|
+
total_elevation = run_data['elevation']
|
|
305
|
+
computed_distance = run_data['distance']
|
|
304
306
|
|
|
305
307
|
if wgs_points.any?
|
|
306
308
|
if wgs_points.any? { |pt| pt[3] }
|
|
@@ -321,44 +323,44 @@ module GitFit
|
|
|
321
323
|
distance: computed_distance&.round(2),
|
|
322
324
|
moving_time: moving_time,
|
|
323
325
|
elapsed_time: elapsed_time,
|
|
324
|
-
sport_category: TYPE_MAP[data_type] ||
|
|
326
|
+
sport_category: TYPE_MAP[data_type] || 'other',
|
|
325
327
|
sport_type: data_type,
|
|
326
328
|
start_date: start_date.iso8601,
|
|
327
329
|
start_date_local: local_time_from_gps(start_date, wgs_points),
|
|
328
|
-
location_country: run_data[
|
|
330
|
+
location_country: run_data['region'],
|
|
329
331
|
summary_polyline: polyline_str,
|
|
330
332
|
average_heartrate: avg_hr&.round(1),
|
|
331
333
|
average_speed: computed_distance && moving_time&.positive? ? (computed_distance.to_f / moving_time).round(2) : nil,
|
|
332
334
|
elevation_gain: total_elevation&.round(1),
|
|
333
|
-
source:
|
|
335
|
+
source: 'keep',
|
|
334
336
|
}
|
|
335
337
|
end
|
|
336
338
|
|
|
337
339
|
def write_standardized_json(wgs_points, payload, run_data, keep_id)
|
|
338
340
|
geo_points = payload[:geo_points] || []
|
|
339
341
|
hr_data = payload[:hr_data]
|
|
340
|
-
start_time = run_data[
|
|
342
|
+
start_time = run_data['startTime']
|
|
341
343
|
|
|
342
344
|
extended = wgs_points.each_with_index.map do |pt, i|
|
|
343
345
|
orig = geo_points[i] || {}
|
|
344
346
|
ts = pt[3]
|
|
345
347
|
{
|
|
346
348
|
latitude: pt[0], longitude: pt[1], altitude: pt[2],
|
|
347
|
-
timestamp: ts&.strftime(
|
|
349
|
+
timestamp: ts&.strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
348
350
|
heartrate: pt[4],
|
|
349
|
-
speed: orig[
|
|
350
|
-
currentTotalDistance: orig[
|
|
351
|
-
currentTotalDuration: orig[
|
|
352
|
-
currentPace: orig[
|
|
353
|
-
pressure: orig[
|
|
354
|
-
accuracyRadius: orig[
|
|
355
|
-
verticalAccuracy: orig[
|
|
356
|
-
locationType: orig[
|
|
357
|
-
isPause: orig[
|
|
358
|
-
flags: orig[
|
|
359
|
-
processLabel: orig[
|
|
360
|
-
crossKmMark: orig[
|
|
361
|
-
currentTotalSteps: orig[
|
|
351
|
+
speed: orig['speed'],
|
|
352
|
+
currentTotalDistance: orig['currentTotalDistance'],
|
|
353
|
+
currentTotalDuration: orig['currentTotalDuration'],
|
|
354
|
+
currentPace: orig['currentPace'],
|
|
355
|
+
pressure: orig['pressure'],
|
|
356
|
+
accuracyRadius: orig['accuracyRadius'],
|
|
357
|
+
verticalAccuracy: orig['verticalAccuracy'],
|
|
358
|
+
locationType: orig['locationType'],
|
|
359
|
+
isPause: orig['isPause'],
|
|
360
|
+
flags: orig['flags'],
|
|
361
|
+
processLabel: orig['processLabel'],
|
|
362
|
+
crossKmMark: orig['crossKmMark'],
|
|
363
|
+
currentTotalSteps: orig['currentTotalSteps']
|
|
362
364
|
}
|
|
363
365
|
end
|
|
364
366
|
|
|
@@ -376,7 +378,7 @@ module GitFit
|
|
|
376
378
|
|
|
377
379
|
def extract_id(id_str)
|
|
378
380
|
return nil unless id_str
|
|
379
|
-
id_str.split(
|
|
381
|
+
id_str.split('_')[1]
|
|
380
382
|
end
|
|
381
383
|
|
|
382
384
|
def decode_geo_points(geo_data)
|
|
@@ -399,7 +401,7 @@ module GitFit
|
|
|
399
401
|
end
|
|
400
402
|
|
|
401
403
|
def aes_decrypt(data)
|
|
402
|
-
cipher = OpenSSL::Cipher.new(
|
|
404
|
+
cipher = OpenSSL::Cipher.new('AES-128-CBC')
|
|
403
405
|
cipher.decrypt
|
|
404
406
|
cipher.key = AES_KEY
|
|
405
407
|
cipher.iv = AES_IV
|
|
@@ -418,7 +420,7 @@ module GitFit
|
|
|
418
420
|
next
|
|
419
421
|
end
|
|
420
422
|
end
|
|
421
|
-
raise Zlib::DataError,
|
|
423
|
+
raise Zlib::DataError, 'cannot inflate data'
|
|
422
424
|
end
|
|
423
425
|
|
|
424
426
|
def find_nearest_hr(hr_data, geo_ts, start_time, threshold: HR_THRESHOLD)
|
|
@@ -438,7 +440,7 @@ module GitFit
|
|
|
438
440
|
min_diff = Float::INFINITY
|
|
439
441
|
|
|
440
442
|
hr_data.each do |item|
|
|
441
|
-
ts = item[
|
|
443
|
+
ts = item['timestamp']
|
|
442
444
|
next unless ts
|
|
443
445
|
|
|
444
446
|
diff = (normalize.call(ts) - target).abs
|
|
@@ -448,7 +450,7 @@ module GitFit
|
|
|
448
450
|
end
|
|
449
451
|
end
|
|
450
452
|
|
|
451
|
-
hr = nearest&.dig(
|
|
453
|
+
hr = nearest&.dig('beatsPerMinute')
|
|
452
454
|
hr&.positive? ? hr : nil
|
|
453
455
|
end
|
|
454
456
|
|
data/lib/git_fit/sync/lorem.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'time'
|
|
4
6
|
|
|
5
7
|
module GitFit
|
|
6
8
|
module Sync
|
|
@@ -9,41 +11,41 @@ module GitFit
|
|
|
9
11
|
register_test_adapter
|
|
10
12
|
|
|
11
13
|
DEMO_ACTIVITIES = [
|
|
12
|
-
{ id:
|
|
14
|
+
{ id: 'alpha_001', name: 'Lorem Morning Run', sport_category: 'run',
|
|
13
15
|
distance: 5200.0, moving_time: 1800, elapsed_time: 2000,
|
|
14
|
-
start_date:
|
|
15
|
-
location_country:
|
|
16
|
+
start_date: '2025-01-15T06:30:00Z', start_date_local: '2025-01-15T14:30:00+08:00',
|
|
17
|
+
location_country: 'CN', average_heartrate: 148.0, max_heartrate: 165.0,
|
|
16
18
|
average_cadence: 172.0, max_cadence: 185.0, average_power: nil, max_power: nil,
|
|
17
19
|
average_temperature: 22.0, calories: 380,
|
|
18
|
-
average_speed: 5200.0 / 1800, elevation_gain: 125.0, external_id:
|
|
19
|
-
{ id:
|
|
20
|
+
average_speed: 5200.0 / 1800, elevation_gain: 125.0, external_id: 'lorem_001' },
|
|
21
|
+
{ id: 'beta_002', name: 'Ipsum Cycling Session', sport_category: 'ride',
|
|
20
22
|
distance: 35000.0, moving_time: 5400, elapsed_time: 6000,
|
|
21
|
-
start_date:
|
|
22
|
-
location_country:
|
|
23
|
+
start_date: '2025-01-14T08:00:00Z', start_date_local: '2025-01-14T16:00:00+08:00',
|
|
24
|
+
location_country: 'CN', average_heartrate: 138.0, max_heartrate: 155.0,
|
|
23
25
|
average_cadence: 85.0, max_cadence: 105.0, average_power: 185.0, max_power: 420.0,
|
|
24
26
|
average_temperature: 18.0, calories: 850,
|
|
25
|
-
average_speed: 35000.0 / 5400, elevation_gain: 450.0, external_id:
|
|
26
|
-
{ id:
|
|
27
|
+
average_speed: 35000.0 / 5400, elevation_gain: 450.0, external_id: 'lorem_002' },
|
|
28
|
+
{ id: 'gamma_003', name: 'Dolor Hike', sport_category: 'hike',
|
|
27
29
|
distance: 12000.0, moving_time: 7200, elapsed_time: 8100,
|
|
28
|
-
start_date:
|
|
29
|
-
location_country:
|
|
30
|
+
start_date: '2025-01-13T09:00:00Z', start_date_local: '2025-01-13T17:00:00+08:00',
|
|
31
|
+
location_country: 'CN', average_heartrate: 122.0, max_heartrate: 142.0,
|
|
30
32
|
average_cadence: 110.0, max_cadence: 135.0, average_power: nil, max_power: nil,
|
|
31
33
|
average_temperature: 15.0, calories: 620,
|
|
32
|
-
average_speed: 12000.0 / 7200, elevation_gain: 820.0, external_id:
|
|
33
|
-
{ id:
|
|
34
|
+
average_speed: 12000.0 / 7200, elevation_gain: 820.0, external_id: 'lorem_003' },
|
|
35
|
+
{ id: 'delta_004', name: 'Sit Amet Walk', sport_category: 'walk',
|
|
34
36
|
distance: 5000.0, moving_time: 3600, elapsed_time: 3900,
|
|
35
|
-
start_date:
|
|
36
|
-
location_country:
|
|
37
|
+
start_date: '2025-01-12T18:00:00Z', start_date_local: '2025-01-12T18:00:00+08:00',
|
|
38
|
+
location_country: 'CN', average_heartrate: 108.0, max_heartrate: 125.0,
|
|
37
39
|
average_cadence: 105.0, max_cadence: 118.0, average_power: nil, max_power: nil,
|
|
38
40
|
average_temperature: 20.0, calories: 280,
|
|
39
|
-
average_speed: 5000.0 / 3600, elevation_gain: 35.0, external_id:
|
|
40
|
-
{ id:
|
|
41
|
+
average_speed: 5000.0 / 3600, elevation_gain: 35.0, external_id: 'lorem_004' },
|
|
42
|
+
{ id: 'epsilon_005', name: 'Consectetur Swim', sport_category: 'swim',
|
|
41
43
|
distance: 2000.0, moving_time: 2400, elapsed_time: 2700,
|
|
42
|
-
start_date:
|
|
43
|
-
location_country:
|
|
44
|
+
start_date: '2025-01-11T07:00:00Z', start_date_local: '2025-01-11T15:00:00+08:00',
|
|
45
|
+
location_country: 'CN', average_heartrate: 135.0, max_heartrate: 150.0,
|
|
44
46
|
average_cadence: 28.0, max_cadence: 32.0, average_power: nil, max_power: nil,
|
|
45
47
|
average_temperature: 26.0, calories: 520,
|
|
46
|
-
average_speed: 2000.0 / 2400, elevation_gain: 0.0, external_id:
|
|
48
|
+
average_speed: 2000.0 / 2400, elevation_gain: 0.0, external_id: 'lorem_005' }
|
|
47
49
|
].freeze
|
|
48
50
|
|
|
49
51
|
def authenticate
|
|
@@ -97,8 +99,8 @@ module GitFit
|
|
|
97
99
|
calories: act[:calories],
|
|
98
100
|
average_speed: act[:average_speed],
|
|
99
101
|
elevation_gain: act[:elevation_gain],
|
|
100
|
-
source:
|
|
101
|
-
external_id: act[:external_id]
|
|
102
|
+
source: 'lorem',
|
|
103
|
+
external_id: act[:external_id],
|
|
102
104
|
}
|
|
103
105
|
end
|
|
104
106
|
|
|
@@ -108,13 +110,13 @@ module GitFit
|
|
|
108
110
|
num_points = [(distance / 500.0).ceil, 20].max
|
|
109
111
|
|
|
110
112
|
case category
|
|
111
|
-
when
|
|
113
|
+
when 'run'
|
|
112
114
|
(0...num_points).map { |i| [base_lat + i * 0.0002, base_lng + i * 0.0003] }
|
|
113
|
-
when
|
|
115
|
+
when 'ride'
|
|
114
116
|
(0...num_points).map { |i| [base_lat + i * 0.0005, base_lng + i * 0.0008] }
|
|
115
|
-
when
|
|
117
|
+
when 'hike'
|
|
116
118
|
(0...num_points).map { |i| [base_lat + i * 0.0003 + Math.sin(i * 0.5) * 0.0001, base_lng + i * 0.0004] }
|
|
117
|
-
when
|
|
119
|
+
when 'swim'
|
|
118
120
|
[[base_lat, base_lng]]
|
|
119
121
|
else
|
|
120
122
|
(0...num_points).map { |i| [base_lat + i * 0.0002, base_lng + i * 0.0002] }
|
data/lib/git_fit/sync/runner.rb
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thread'
|
|
2
4
|
|
|
3
5
|
module GitFit
|
|
4
6
|
module Sync
|
|
@@ -76,29 +78,30 @@ module GitFit
|
|
|
76
78
|
actual_key = klass&.config_key || source
|
|
77
79
|
cfg = @config.sync_config(actual_key)
|
|
78
80
|
unless klass
|
|
79
|
-
output_queue << { type: :banner, source:, text:
|
|
81
|
+
output_queue << { type: :banner, source:, text: 'no adapter' }
|
|
80
82
|
clear_source(source)
|
|
81
83
|
return false
|
|
82
84
|
end
|
|
83
85
|
|
|
84
|
-
output_queue << { type: :banner, source:, text:
|
|
85
|
-
adapter = klass.new(config: cfg, db: @db, activity_filter: @activity_filter, privacy: @privacy,
|
|
86
|
+
output_queue << { type: :banner, source:, text: 'authenticating' }
|
|
87
|
+
adapter = klass.new(config: cfg, db: @db, activity_filter: @activity_filter, privacy: @privacy,
|
|
88
|
+
time_budget: @time_budget)
|
|
86
89
|
unless adapter.before_call
|
|
87
|
-
output_queue << { type: :banner, source:, text:
|
|
90
|
+
output_queue << { type: :banner, source:, text: 'before_call failed' }
|
|
88
91
|
clear_source(source)
|
|
89
92
|
return false
|
|
90
93
|
end
|
|
91
94
|
|
|
92
95
|
unless adapter.authenticate
|
|
93
|
-
output_queue << { type: :banner, source:, text:
|
|
96
|
+
output_queue << { type: :banner, source:, text: 'auth failed' }
|
|
94
97
|
clear_source(source)
|
|
95
98
|
return false
|
|
96
99
|
end
|
|
97
100
|
|
|
98
|
-
output_queue << { type: :banner, source:, text:
|
|
101
|
+
output_queue << { type: :banner, source:, text: 'fetching activity list' }
|
|
99
102
|
ids = adapter.pending_ids
|
|
100
103
|
if ids.nil? || ids.empty?
|
|
101
|
-
output_queue << { type: :banner, source:, text:
|
|
104
|
+
output_queue << { type: :banner, source:, text: 'no new activities' }
|
|
102
105
|
clear_source(source)
|
|
103
106
|
return false
|
|
104
107
|
end
|
|
@@ -125,7 +128,7 @@ module GitFit
|
|
|
125
128
|
|
|
126
129
|
begin
|
|
127
130
|
process_source(source)
|
|
128
|
-
rescue => e
|
|
131
|
+
rescue StandardError => e
|
|
129
132
|
@output_queue << { type: :error, source:, message: "Worker crashed: #{e.message}" }
|
|
130
133
|
@stats_lock.synchronize do
|
|
131
134
|
@pending_ids.delete(source)
|
|
@@ -194,7 +197,7 @@ module GitFit
|
|
|
194
197
|
info: nil, done: processed, total: total
|
|
195
198
|
}
|
|
196
199
|
end
|
|
197
|
-
rescue => e
|
|
200
|
+
rescue StandardError => e
|
|
198
201
|
@output_queue << { type: :error, source:, message: e.message }
|
|
199
202
|
end
|
|
200
203
|
|
|
@@ -306,7 +309,7 @@ module GitFit
|
|
|
306
309
|
end
|
|
307
310
|
end
|
|
308
311
|
$stdout.flush
|
|
309
|
-
rescue => e
|
|
312
|
+
rescue StandardError => e
|
|
310
313
|
$stdout.puts " Runner output error: #{e.message}"
|
|
311
314
|
$stdout.flush
|
|
312
315
|
end
|