git-fit 0.10.3 → 0.10.4
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/source/keep.rb +117 -0
- data/lib/git_fit/source.rb +1 -0
- data/lib/git_fit/sync/keep.rb +4 -106
- data/lib/git_fit/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 69674e0fc800279e2099fe879318f4b8f3ce751325515192474dc40223438293
|
|
4
|
+
data.tar.gz: f91fd18620742288f923f302917667121d310a5707ac71cc185f77da5fc154a6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f204350fdd449be0942ba5d42917a54460198f61ffdfde00b6c51fa39cdb379c0e056851c05e0bae6577b2d3dca9a0428bc967247245528c65c94b1b9f32d591
|
|
7
|
+
data.tar.gz: 9d4bf7e3c1c72ebfc4a35c05e2b5c150b87c17cfd67ca634ede7e4026b72978c89360434baac73b9e36addd1f88b8c61b8820c0089712086f9cc9dee8777e989
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'zlib'
|
|
5
|
+
require 'openssl'
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'time'
|
|
8
|
+
require_relative '../geo/coord_transform'
|
|
9
|
+
|
|
10
|
+
module GitFit
|
|
11
|
+
module Source
|
|
12
|
+
module Keep
|
|
13
|
+
PLACEHOLDER_POLYLINE = 'gqqrFurkeU??'
|
|
14
|
+
|
|
15
|
+
AES_KEY = Base64.decode64('NTZmZTU5OzgyZzpkODczYw==').freeze
|
|
16
|
+
AES_IV = Base64.decode64('MjM0Njg5MjQzMjkyMDMwMA==').freeze
|
|
17
|
+
|
|
18
|
+
TIMESTAMP_THRESHOLD = 3_600_000
|
|
19
|
+
HR_THRESHOLD = 100
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
def decode_geo_points(geo_data)
|
|
24
|
+
return nil unless geo_data
|
|
25
|
+
raw = Base64.decode64(geo_data)
|
|
26
|
+
decrypted = aes_decrypt(raw)
|
|
27
|
+
decompressed = zlib_inflate(decrypted)
|
|
28
|
+
JSON.parse(decompressed)
|
|
29
|
+
rescue StandardError
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def decode_hr_data(hr_data)
|
|
34
|
+
return nil unless hr_data
|
|
35
|
+
raw = Base64.decode64(hr_data)
|
|
36
|
+
decompressed = zlib_inflate(raw)
|
|
37
|
+
JSON.parse(decompressed)
|
|
38
|
+
rescue StandardError
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def aes_decrypt(data)
|
|
43
|
+
cipher = OpenSSL::Cipher.new('AES-128-CBC')
|
|
44
|
+
cipher.decrypt
|
|
45
|
+
cipher.key = AES_KEY
|
|
46
|
+
cipher.iv = AES_IV
|
|
47
|
+
cipher.update(data) + cipher.final
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def zlib_inflate(data)
|
|
51
|
+
[-Zlib::MAX_WBITS, Zlib::MAX_WBITS, Zlib::MAX_WBITS + 16].each do |wbits|
|
|
52
|
+
inflater = Zlib::Inflate.new(wbits)
|
|
53
|
+
result = inflater.inflate(data)
|
|
54
|
+
begin
|
|
55
|
+
inflater.finish
|
|
56
|
+
rescue StandardError
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
inflater.close
|
|
60
|
+
return result
|
|
61
|
+
rescue Zlib::DataError
|
|
62
|
+
next
|
|
63
|
+
end
|
|
64
|
+
raise Zlib::DataError, 'cannot inflate data'
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def compute_wgs_points(payload, run_data)
|
|
68
|
+
geo_points = payload[:geo_points]
|
|
69
|
+
hr_data = payload[:hr_data]
|
|
70
|
+
return [] unless geo_points&.any?
|
|
71
|
+
|
|
72
|
+
start_time = run_data['startTime']
|
|
73
|
+
use_absolute_ts = geo_points.first['timestamp'].to_i > TIMESTAMP_THRESHOLD
|
|
74
|
+
effective_start = use_absolute_ts ? 0 : start_time
|
|
75
|
+
|
|
76
|
+
geo_points.map do |p|
|
|
77
|
+
lat, lng = Geo::CoordTransform.gcj02_to_wgs84_exact(p['latitude'], p['longitude'])
|
|
78
|
+
ts = p['timestamp'] || p['unixTimestamp']
|
|
79
|
+
pt_time = ts ? Time.at(effective_start / 1000.0 + ts.to_i / 10.0).utc : nil
|
|
80
|
+
hr = find_nearest_hr(hr_data, ts, start_time)
|
|
81
|
+
[lat, lng, p['altitude'], pt_time, hr]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def find_nearest_hr(hr_data, geo_ts, start_time, threshold: HR_THRESHOLD)
|
|
86
|
+
return nil unless hr_data&.any? && geo_ts
|
|
87
|
+
|
|
88
|
+
normalize = lambda { |ts|
|
|
89
|
+
if ts > TIMESTAMP_THRESHOLD
|
|
90
|
+
ts - start_time / 100
|
|
91
|
+
else
|
|
92
|
+
ts
|
|
93
|
+
end
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
target = normalize.call(geo_ts)
|
|
97
|
+
|
|
98
|
+
nearest = nil
|
|
99
|
+
min_diff = Float::INFINITY
|
|
100
|
+
|
|
101
|
+
hr_data.each do |item|
|
|
102
|
+
ts = item['timestamp']
|
|
103
|
+
next unless ts
|
|
104
|
+
|
|
105
|
+
diff = (normalize.call(ts) - target).abs
|
|
106
|
+
if diff <= threshold && diff < min_diff
|
|
107
|
+
nearest = item
|
|
108
|
+
min_diff = diff
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
hr = nearest&.dig('beatsPerMinute')
|
|
113
|
+
hr&.positive? ? hr : nil
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
data/lib/git_fit/source.rb
CHANGED
data/lib/git_fit/sync/keep.rb
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'base'
|
|
4
|
-
require 'base64'
|
|
5
|
-
require 'zlib'
|
|
6
|
-
require 'openssl'
|
|
7
4
|
require 'time'
|
|
8
5
|
require 'tzinfo'
|
|
9
6
|
|
|
@@ -14,11 +11,6 @@ module GitFit
|
|
|
14
11
|
STATS_URL = 'https://api.gotokeep.com/pd/v3/stats/detail?dateUnit=all&type=%{sport}&lastDate=%{last_date}'
|
|
15
12
|
LOG_URL = 'https://api.gotokeep.com/pd/v3/%{sport}log/%{run_id}'
|
|
16
13
|
|
|
17
|
-
AES_KEY = Base64.decode64('NTZmZTU5OzgyZzpkODczYw==')
|
|
18
|
-
AES_IV = Base64.decode64('MjM0Njg5MjQzMjkyMDMwMA==')
|
|
19
|
-
|
|
20
|
-
PLACEHOLDER_POLYLINE = 'gqqrFurkeU??'
|
|
21
|
-
|
|
22
14
|
KEEP_TYPES = %w[running hiking cycling].freeze
|
|
23
15
|
KEEP_RAW_EXT = 'json'
|
|
24
16
|
|
|
@@ -38,9 +30,6 @@ module GitFit
|
|
|
38
30
|
'mountaineering' => 'Hiking',
|
|
39
31
|
}.freeze
|
|
40
32
|
|
|
41
|
-
TIMESTAMP_THRESHOLD = 3_600_000
|
|
42
|
-
HR_THRESHOLD = 100
|
|
43
|
-
|
|
44
33
|
register_adapter
|
|
45
34
|
register_config :phone, :password
|
|
46
35
|
|
|
@@ -231,7 +220,7 @@ module GitFit
|
|
|
231
220
|
heartRates: payload[:hr_data],
|
|
232
221
|
}, keep_id)
|
|
233
222
|
|
|
234
|
-
wgs_points = compute_wgs_points(payload, run_data)
|
|
223
|
+
wgs_points = Source::Keep.compute_wgs_points(payload, run_data)
|
|
235
224
|
write_standardized_json(wgs_points, payload, run_data, keep_id)
|
|
236
225
|
|
|
237
226
|
attrs = build_activity_from_wgs(run_data, wgs_points)
|
|
@@ -273,35 +262,17 @@ module GitFit
|
|
|
273
262
|
else
|
|
274
263
|
payload = { geo_points: raw['geoPoints'], hr_data: raw['heartRates'] }
|
|
275
264
|
run_data = raw['meta']
|
|
276
|
-
compute_wgs_points(payload, run_data)
|
|
265
|
+
Source::Keep.compute_wgs_points(payload, run_data)
|
|
277
266
|
end
|
|
278
267
|
end
|
|
279
268
|
|
|
280
269
|
def decrypt_raw_payload(run_data)
|
|
281
270
|
{
|
|
282
|
-
geo_points: decode_geo_points(run_data['geoPoints']),
|
|
283
|
-
hr_data: decode_hr_data(run_data.dig('heartRate', 'heartRates')),
|
|
271
|
+
geo_points: Source::Keep.decode_geo_points(run_data['geoPoints']),
|
|
272
|
+
hr_data: Source::Keep.decode_hr_data(run_data.dig('heartRate', 'heartRates')),
|
|
284
273
|
}
|
|
285
274
|
end
|
|
286
275
|
|
|
287
|
-
def compute_wgs_points(payload, run_data)
|
|
288
|
-
geo_points = payload[:geo_points]
|
|
289
|
-
hr_data = payload[:hr_data]
|
|
290
|
-
return [] unless geo_points&.any?
|
|
291
|
-
|
|
292
|
-
start_time = run_data['startTime']
|
|
293
|
-
use_absolute_ts = geo_points.first['timestamp'].to_i > TIMESTAMP_THRESHOLD
|
|
294
|
-
effective_start = use_absolute_ts ? 0 : start_time
|
|
295
|
-
|
|
296
|
-
geo_points.map do |p|
|
|
297
|
-
lat, lng = Geo::CoordTransform.gcj02_to_wgs84_exact(p['latitude'], p['longitude'])
|
|
298
|
-
ts = p['timestamp'] || p['unixTimestamp']
|
|
299
|
-
pt_time = ts ? Time.at(effective_start / 1000.0 + ts.to_i / 10.0).utc : nil
|
|
300
|
-
hr = find_nearest_hr(hr_data, ts, start_time)
|
|
301
|
-
[lat, lng, p['altitude'], pt_time, hr]
|
|
302
|
-
end
|
|
303
|
-
end
|
|
304
|
-
|
|
305
276
|
def tz_resolver
|
|
306
277
|
@tz_resolver ||= GitFit::Timezone::Resolver.new({})
|
|
307
278
|
end
|
|
@@ -414,79 +385,6 @@ module GitFit
|
|
|
414
385
|
id_str.split('_')[1]
|
|
415
386
|
end
|
|
416
387
|
|
|
417
|
-
def decode_geo_points(geo_data)
|
|
418
|
-
return nil unless geo_data
|
|
419
|
-
raw = Base64.decode64(geo_data)
|
|
420
|
-
decrypted = aes_decrypt(raw)
|
|
421
|
-
decompressed = zlib_inflate(decrypted)
|
|
422
|
-
JSON.parse(decompressed)
|
|
423
|
-
rescue StandardError
|
|
424
|
-
nil
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
def decode_hr_data(hr_data)
|
|
428
|
-
return nil unless hr_data
|
|
429
|
-
raw = Base64.decode64(hr_data)
|
|
430
|
-
decompressed = zlib_inflate(raw)
|
|
431
|
-
JSON.parse(decompressed)
|
|
432
|
-
rescue StandardError
|
|
433
|
-
nil
|
|
434
|
-
end
|
|
435
|
-
|
|
436
|
-
def aes_decrypt(data)
|
|
437
|
-
cipher = OpenSSL::Cipher.new('AES-128-CBC')
|
|
438
|
-
cipher.decrypt
|
|
439
|
-
cipher.key = AES_KEY
|
|
440
|
-
cipher.iv = AES_IV
|
|
441
|
-
cipher.update(data) + cipher.final
|
|
442
|
-
end
|
|
443
|
-
|
|
444
|
-
def zlib_inflate(data)
|
|
445
|
-
[-Zlib::MAX_WBITS, Zlib::MAX_WBITS, Zlib::MAX_WBITS + 16].each do |wbits|
|
|
446
|
-
begin
|
|
447
|
-
inflater = Zlib::Inflate.new(wbits)
|
|
448
|
-
result = inflater.inflate(data)
|
|
449
|
-
inflater.finish rescue nil
|
|
450
|
-
inflater.close
|
|
451
|
-
return result
|
|
452
|
-
rescue Zlib::DataError
|
|
453
|
-
next
|
|
454
|
-
end
|
|
455
|
-
end
|
|
456
|
-
raise Zlib::DataError, 'cannot inflate data'
|
|
457
|
-
end
|
|
458
|
-
|
|
459
|
-
def find_nearest_hr(hr_data, geo_ts, start_time, threshold: HR_THRESHOLD)
|
|
460
|
-
return nil unless hr_data&.any? && geo_ts
|
|
461
|
-
|
|
462
|
-
normalize = ->(ts) {
|
|
463
|
-
if ts > TIMESTAMP_THRESHOLD
|
|
464
|
-
ts - start_time / 100
|
|
465
|
-
else
|
|
466
|
-
ts
|
|
467
|
-
end
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
target = normalize.call(geo_ts)
|
|
471
|
-
|
|
472
|
-
nearest = nil
|
|
473
|
-
min_diff = Float::INFINITY
|
|
474
|
-
|
|
475
|
-
hr_data.each do |item|
|
|
476
|
-
ts = item['timestamp']
|
|
477
|
-
next unless ts
|
|
478
|
-
|
|
479
|
-
diff = (normalize.call(ts) - target).abs
|
|
480
|
-
if diff <= threshold && diff < min_diff
|
|
481
|
-
nearest = item
|
|
482
|
-
min_diff = diff
|
|
483
|
-
end
|
|
484
|
-
end
|
|
485
|
-
|
|
486
|
-
hr = nearest&.dig('beatsPerMinute')
|
|
487
|
-
hr&.positive? ? hr : nil
|
|
488
|
-
end
|
|
489
|
-
|
|
490
388
|
def compute_avg_hr(points)
|
|
491
389
|
hrs = points.map { |p| p[4] }.compact
|
|
492
390
|
return nil if hrs.empty?
|
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.10.
|
|
4
|
+
version: 0.10.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lax
|
|
@@ -262,6 +262,7 @@ files:
|
|
|
262
262
|
- lib/git_fit/privacy/polyline_filter.rb
|
|
263
263
|
- lib/git_fit/source.rb
|
|
264
264
|
- lib/git_fit/source/base.rb
|
|
265
|
+
- lib/git_fit/source/keep.rb
|
|
265
266
|
- lib/git_fit/source/tally.rb
|
|
266
267
|
- lib/git_fit/sport_mapper.rb
|
|
267
268
|
- lib/git_fit/strava_web/file_check.rb
|