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
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'garmin_base'
|
|
4
|
+
require 'open3'
|
|
3
5
|
|
|
4
6
|
module GitFit
|
|
5
7
|
module Sync
|
|
@@ -31,16 +33,16 @@ module GitFit
|
|
|
31
33
|
end
|
|
32
34
|
end
|
|
33
35
|
|
|
34
|
-
puts
|
|
35
|
-
puts
|
|
36
|
-
puts
|
|
36
|
+
puts 'Garmin: no valid DI token found'
|
|
37
|
+
puts ' First time: ./scripts/garmin_auth_local.sh'
|
|
38
|
+
puts ' Refresh: ./scripts/garmin_auth_local.sh --sync'
|
|
37
39
|
false
|
|
38
40
|
end
|
|
39
41
|
|
|
40
42
|
def before_call
|
|
41
|
-
if @config[
|
|
42
|
-
puts
|
|
43
|
-
puts
|
|
43
|
+
if @config['auth_seed'].to_s.empty? && !File.exist?(token_path)
|
|
44
|
+
puts 'Garmin: auth_seed not configured and no cached token'
|
|
45
|
+
puts ' Run: ./scripts/garmin_auth_local.sh'
|
|
44
46
|
return false
|
|
45
47
|
end
|
|
46
48
|
@start_time = Time.now
|
|
@@ -48,7 +50,7 @@ module GitFit
|
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
def reauth_hint
|
|
51
|
-
|
|
53
|
+
'Re-auth: ./scripts/garmin_auth_local.sh --sync'
|
|
52
54
|
end
|
|
53
55
|
|
|
54
56
|
private
|
|
@@ -64,30 +66,30 @@ module GitFit
|
|
|
64
66
|
data = JSON.parse(File.read(path))
|
|
65
67
|
@access_token = data
|
|
66
68
|
true
|
|
67
|
-
rescue => e
|
|
69
|
+
rescue StandardError => e
|
|
68
70
|
puts "Failed to load Garmin tokens: #{e.message}"
|
|
69
71
|
false
|
|
70
72
|
end
|
|
71
73
|
|
|
72
74
|
def save_tokens
|
|
73
|
-
return unless @access_token&.dig(
|
|
75
|
+
return unless @access_token&.dig('access_token')
|
|
74
76
|
|
|
75
77
|
path = token_path
|
|
76
78
|
FileUtils.mkdir_p(File.dirname(path))
|
|
77
79
|
File.write(path, JSON.pretty_generate(@access_token))
|
|
78
|
-
rescue => e
|
|
80
|
+
rescue StandardError => e
|
|
79
81
|
puts "Failed to save Garmin tokens: #{e.message}"
|
|
80
82
|
end
|
|
81
83
|
|
|
82
84
|
def remove_stale_tokens
|
|
83
85
|
path = token_path
|
|
84
86
|
File.delete(path) if File.exist?(path)
|
|
85
|
-
rescue => e
|
|
87
|
+
rescue StandardError => e
|
|
86
88
|
puts "Warning: failed to remove stale tokens: #{e.message}"
|
|
87
89
|
end
|
|
88
90
|
|
|
89
91
|
def load_auth_seed
|
|
90
|
-
seed = @config[
|
|
92
|
+
seed = @config['auth_seed']
|
|
91
93
|
return false unless seed && !seed.empty?
|
|
92
94
|
|
|
93
95
|
decoded = JSON.parse(Base64.strict_decode64(seed))
|
|
@@ -95,49 +97,49 @@ module GitFit
|
|
|
95
97
|
save_tokens
|
|
96
98
|
true
|
|
97
99
|
rescue JSON::ParserError
|
|
98
|
-
puts
|
|
99
|
-
puts
|
|
100
|
+
puts 'Garmin: auth_seed decode failed — invalid Base64 or JSON'
|
|
101
|
+
puts ' Fix: ./scripts/garmin_auth_local.sh --sync'
|
|
100
102
|
false
|
|
101
|
-
rescue => e
|
|
103
|
+
rescue StandardError => e
|
|
102
104
|
puts "Garmin: auth_seed error: #{e.message}"
|
|
103
105
|
false
|
|
104
106
|
end
|
|
105
107
|
|
|
106
108
|
def refresh_token
|
|
107
|
-
return false unless @access_token&.dig(
|
|
109
|
+
return false unless @access_token&.dig('refresh_token')
|
|
108
110
|
|
|
109
|
-
client_id = @access_token[
|
|
111
|
+
client_id = @access_token['di_client_id'] || DI_CLIENT_IDS.first
|
|
110
112
|
basic = Base64.strict_encode64("#{client_id}:")
|
|
111
113
|
|
|
112
114
|
uri = URI("#{auth_base}/di-oauth2-service/oauth/token")
|
|
113
115
|
req = Net::HTTP::Post.new(uri)
|
|
114
|
-
req[
|
|
115
|
-
req[
|
|
116
|
-
req[
|
|
117
|
-
req[
|
|
116
|
+
req['Authorization'] = "Basic #{basic}"
|
|
117
|
+
req['Content-Type'] = 'application/x-www-form-urlencoded'
|
|
118
|
+
req['Cache-Control'] = 'no-cache'
|
|
119
|
+
req['User-Agent'] = 'GCM-Android-5.23'
|
|
118
120
|
req.body = URI.encode_www_form(
|
|
119
|
-
grant_type:
|
|
121
|
+
grant_type: 'refresh_token',
|
|
120
122
|
client_id: client_id,
|
|
121
|
-
refresh_token: @access_token[
|
|
123
|
+
refresh_token: @access_token['refresh_token']
|
|
122
124
|
)
|
|
123
125
|
|
|
124
126
|
resp = retry_on_429 { send_http(uri, req) }
|
|
125
127
|
unless resp.code.to_i == 200
|
|
126
128
|
msg = "DI refresh error: #{resp.code}"
|
|
127
129
|
if resp.code.to_i == 400
|
|
128
|
-
msg +=
|
|
129
|
-
msg +=
|
|
130
|
+
msg += ' — refresh_token expired across rotation'
|
|
131
|
+
msg += ', re-run: scripts/garmin_auth_local.sh'
|
|
130
132
|
elsif resp.code.to_i == 401
|
|
131
|
-
msg +=
|
|
132
|
-
msg +=
|
|
133
|
+
msg += ' — DI client_id rotated by Garmin'
|
|
134
|
+
msg += ', re-run: scripts/garmin_auth_local.sh'
|
|
133
135
|
end
|
|
134
136
|
raise AuthError, msg
|
|
135
137
|
end
|
|
136
138
|
|
|
137
139
|
result = JSON.parse(resp.body)
|
|
138
|
-
result[
|
|
139
|
-
if result[
|
|
140
|
-
result[
|
|
140
|
+
result['expires_at'] = Time.now.to_i + (result['expires_in'] || 64800).to_i
|
|
141
|
+
if result['refresh_token_expires_in']
|
|
142
|
+
result['refresh_token_expires_at'] = Time.now.to_i + result['refresh_token_expires_in'].to_i
|
|
141
143
|
end
|
|
142
144
|
@access_token = result
|
|
143
145
|
save_tokens
|
|
@@ -153,31 +155,31 @@ module GitFit
|
|
|
153
155
|
# historical. In scripts/garmin_auth_local.sh, Playwright runs first (cookies
|
|
154
156
|
# persist 365d → no repeat MFA), curl_cffi is the fallback.
|
|
155
157
|
def strategy_a_login
|
|
156
|
-
script = File.expand_path(
|
|
158
|
+
script = File.expand_path('../../../scripts/garmin_auth.py', __dir__)
|
|
157
159
|
return nil unless File.exist?(script)
|
|
158
160
|
|
|
159
161
|
stdout, stderr, status = Open3.capture3(
|
|
160
|
-
{
|
|
161
|
-
|
|
162
|
+
{'SYNC__GARMIN__EMAIL' => @email, 'SYNC__GARMIN__PASSWORD' => @password, 'SYNC__GARMIN__DOMAIN' => @domain},
|
|
163
|
+
'python3', script
|
|
162
164
|
)
|
|
163
165
|
warn stderr unless stderr.empty?
|
|
164
166
|
JSON.parse(stdout) if status.success?
|
|
165
|
-
rescue => e
|
|
167
|
+
rescue StandardError => e
|
|
166
168
|
puts "Strategy A (curl_cffi) failed: #{e.message}"
|
|
167
169
|
nil
|
|
168
170
|
end
|
|
169
171
|
|
|
170
172
|
def strategy_b_login
|
|
171
|
-
script = File.expand_path(
|
|
173
|
+
script = File.expand_path('../../../scripts/garmin_auth_playwright.py', __dir__)
|
|
172
174
|
return nil unless File.exist?(script)
|
|
173
175
|
|
|
174
176
|
stdout, stderr, status = Open3.capture3(
|
|
175
|
-
{
|
|
176
|
-
|
|
177
|
+
{'SYNC__GARMIN__EMAIL' => @email, 'SYNC__GARMIN__PASSWORD' => @password, 'SYNC__GARMIN__DOMAIN' => @domain},
|
|
178
|
+
'python3', script
|
|
177
179
|
)
|
|
178
180
|
warn stderr unless stderr.empty?
|
|
179
181
|
JSON.parse(stdout) if status.success?
|
|
180
|
-
rescue => e
|
|
182
|
+
rescue StandardError => e
|
|
181
183
|
puts "Strategy B (Playwright) failed: #{e.message}"
|
|
182
184
|
nil
|
|
183
185
|
end
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'garmin_base'
|
|
2
4
|
|
|
3
5
|
module GitFit
|
|
4
6
|
module Sync
|
|
@@ -8,20 +10,20 @@ module GitFit
|
|
|
8
10
|
|
|
9
11
|
def initialize(config:, db:, activity_filter: nil, privacy: nil, time_budget: nil)
|
|
10
12
|
super
|
|
11
|
-
@domain =
|
|
12
|
-
@ssl_verify = config.key?(
|
|
13
|
+
@domain = 'garmin.cn'
|
|
14
|
+
@ssl_verify = config.key?('ssl_verify') ? config['ssl_verify'] : false
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
def raw_dir
|
|
16
|
-
File.join(
|
|
18
|
+
File.join('data', 'raw', 'garmin_cn')
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
def std_dir
|
|
20
|
-
File.join(
|
|
22
|
+
File.join('data', 'std', 'garmin_cn')
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def source_prefix
|
|
24
|
-
|
|
26
|
+
'garmin_cn'
|
|
25
27
|
end
|
|
26
28
|
end
|
|
27
29
|
end
|
|
@@ -1,28 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require 'faraday'
|
|
3
5
|
|
|
4
6
|
module GitFit
|
|
5
7
|
module Sync
|
|
6
8
|
class IGPSPORT < Base
|
|
7
|
-
BASE_URL =
|
|
9
|
+
BASE_URL = 'https://prod.zh.igpsport.com/service'
|
|
8
10
|
LOGIN_URL = "#{BASE_URL}/auth/account/login"
|
|
9
11
|
QUERY_URL = "#{BASE_URL}/web-gateway/web-analyze/activity/queryMyActivity"
|
|
10
12
|
DOWNLOAD_URL = "#{BASE_URL}/web-gateway/web-analyze/activity/getDownloadUrl"
|
|
11
13
|
|
|
12
14
|
EXERCISE_MAP = {
|
|
13
|
-
0 =>
|
|
14
|
-
4 =>
|
|
15
|
+
0 => 'ride', 1 => 'run', 2 => 'hike', 3 => 'walk',
|
|
16
|
+
4 => 'swim', 5 => 'ski', 6 => 'workout'
|
|
15
17
|
}.freeze
|
|
16
18
|
|
|
17
|
-
RAW_EXT =
|
|
19
|
+
RAW_EXT = 'fit'
|
|
18
20
|
|
|
19
21
|
register_adapter
|
|
20
22
|
register_config :phone, :password
|
|
21
23
|
|
|
22
24
|
def initialize(...)
|
|
23
25
|
super
|
|
24
|
-
@phone = @config[
|
|
25
|
-
@password = @config[
|
|
26
|
+
@phone = @config['phone']
|
|
27
|
+
@password = @config['password']
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
def before_call
|
|
@@ -42,9 +44,9 @@ module GitFit
|
|
|
42
44
|
acts = fetch_page(page)
|
|
43
45
|
break if acts.nil? || acts.empty?
|
|
44
46
|
acts.each do |act|
|
|
45
|
-
ride_id = act[
|
|
47
|
+
ride_id = act['rideId'].to_s
|
|
46
48
|
next if existing.include?(ride_id) && raw_file_exists?(ride_id)
|
|
47
|
-
type = map_sport(act[
|
|
49
|
+
type = map_sport(act['exerciseType'])
|
|
48
50
|
next if skip_type?(type)
|
|
49
51
|
ids << { id: ride_id, type: type, summary: act }
|
|
50
52
|
end
|
|
@@ -61,14 +63,14 @@ module GitFit
|
|
|
61
63
|
|
|
62
64
|
def login
|
|
63
65
|
resp = Faraday.post(LOGIN_URL,
|
|
64
|
-
JSON.generate({ appId:
|
|
65
|
-
{
|
|
66
|
+
JSON.generate({ appId: 'igpsport-web', username: @phone, password: @password }),
|
|
67
|
+
{ 'Content-Type' => 'application/json' }
|
|
66
68
|
)
|
|
67
69
|
return false unless resp.status == 200
|
|
68
70
|
data = JSON.parse(resp.body)
|
|
69
|
-
@token = data.dig(
|
|
71
|
+
@token = data.dig('data', 'access_token')
|
|
70
72
|
return false unless @token
|
|
71
|
-
@auth_headers = {
|
|
73
|
+
@auth_headers = { 'Authorization' => "Bearer #{@token}" }
|
|
72
74
|
true
|
|
73
75
|
end
|
|
74
76
|
|
|
@@ -88,16 +90,16 @@ module GitFit
|
|
|
88
90
|
wgs_points = FIT::Decoder.decode(fit_path)
|
|
89
91
|
return false if wgs_points.empty?
|
|
90
92
|
|
|
91
|
-
first_ts = wgs_points.first[
|
|
93
|
+
first_ts = wgs_points.first['timestamp']
|
|
92
94
|
if first_ts
|
|
93
|
-
fit_epoch = Time.new(1989, 12, 31, 0, 0, 0,
|
|
95
|
+
fit_epoch = Time.new(1989, 12, 31, 0, 0, 0, '+00:00').to_i
|
|
94
96
|
t = Time.at(fit_epoch + first_ts)
|
|
95
97
|
@fit_start_utc = t.utc
|
|
96
|
-
@fit_start_local = t.getlocal(
|
|
98
|
+
@fit_start_local = t.getlocal('+08:00')
|
|
97
99
|
end
|
|
98
100
|
|
|
99
101
|
write_standardized_json(wgs_points, ride_id)
|
|
100
|
-
polyline = Geo::Polyline.encode(wgs_points.map { |pt| [pt[
|
|
102
|
+
polyline = Geo::Polyline.encode(wgs_points.map { |pt| [pt['positionLat'], pt['positionLong']] })
|
|
101
103
|
sensor_summary = compute_sensor_summary(wgs_points)
|
|
102
104
|
|
|
103
105
|
attrs = build_attrs(act, ride_id, polyline, sensor_summary)
|
|
@@ -110,7 +112,7 @@ module GitFit
|
|
|
110
112
|
def get_download_url(ride_id)
|
|
111
113
|
resp = Faraday.get("#{DOWNLOAD_URL}/#{ride_id}", {}, @auth_headers)
|
|
112
114
|
return nil unless resp.status == 200
|
|
113
|
-
JSON.parse(resp.body)[
|
|
115
|
+
JSON.parse(resp.body)['data']
|
|
114
116
|
end
|
|
115
117
|
|
|
116
118
|
def download_fit(url, ride_id)
|
|
@@ -128,7 +130,7 @@ module GitFit
|
|
|
128
130
|
wgs_points = FIT::Decoder.decode(fit_path)
|
|
129
131
|
return nil if wgs_points.empty?
|
|
130
132
|
|
|
131
|
-
Geo::Polyline.encode(wgs_points.map { |pt| [pt[
|
|
133
|
+
Geo::Polyline.encode(wgs_points.map { |pt| [pt['positionLat'], pt['positionLong']] })
|
|
132
134
|
end
|
|
133
135
|
|
|
134
136
|
def write_standardized_json(wgs_points, ride_id)
|
|
@@ -146,30 +148,30 @@ module GitFit
|
|
|
146
148
|
@auth_headers
|
|
147
149
|
)
|
|
148
150
|
return nil unless resp.status == 200
|
|
149
|
-
JSON.parse(resp.body).dig(
|
|
151
|
+
JSON.parse(resp.body).dig('data', 'rows')
|
|
150
152
|
end
|
|
151
153
|
|
|
152
154
|
def build_attrs(act, ride_id, polyline = nil, sensor = {})
|
|
153
|
-
type = map_sport(act[
|
|
154
|
-
start_t = @fit_start_utc || parse_date(act[
|
|
155
|
+
type = map_sport(act['exerciseType'])
|
|
156
|
+
start_t = @fit_start_utc || parse_date(act['startTime'])
|
|
155
157
|
start_t_local = @fit_start_local || start_t
|
|
156
|
-
distance = act[
|
|
157
|
-
moving_time = (act[
|
|
158
|
-
avg_speed = act[
|
|
158
|
+
distance = act['rideDistance'].to_f
|
|
159
|
+
moving_time = (act['totalMovingTime'] || act['recordTime']).to_i
|
|
160
|
+
avg_speed = act['avgSpeed'].to_f
|
|
159
161
|
|
|
160
162
|
{
|
|
161
163
|
run_id: "igpsport_#{ride_id}",
|
|
162
|
-
name: act[
|
|
164
|
+
name: act['title'] || 'iGPSPORT Activity',
|
|
163
165
|
distance: distance,
|
|
164
166
|
moving_time: moving_time > 0 ? moving_time : nil,
|
|
165
167
|
elapsed_time: moving_time > 0 ? moving_time : nil,
|
|
166
168
|
sport_category: type,
|
|
167
|
-
sport_type: EXERCISE_MAP[act[
|
|
169
|
+
sport_type: EXERCISE_MAP[act['exerciseType']] || 'ride',
|
|
168
170
|
start_date: start_t.iso8601,
|
|
169
171
|
start_date_local: start_t_local.iso8601,
|
|
170
172
|
location_country: nil,
|
|
171
|
-
average_heartrate: act[
|
|
172
|
-
max_heartrate: act[
|
|
173
|
+
average_heartrate: act['avgHrm']&.to_f&.nonzero?,
|
|
174
|
+
max_heartrate: act['maxHrm']&.to_f&.nonzero?,
|
|
173
175
|
average_cadence: sensor[:average_cadence],
|
|
174
176
|
max_cadence: sensor[:max_cadence],
|
|
175
177
|
average_power: sensor[:average_power],
|
|
@@ -177,18 +179,18 @@ module GitFit
|
|
|
177
179
|
calories: sensor[:calories],
|
|
178
180
|
average_temperature: sensor[:average_temperature],
|
|
179
181
|
average_speed: avg_speed > 0 ? avg_speed : nil,
|
|
180
|
-
elevation_gain: act[
|
|
182
|
+
elevation_gain: act['totalAscent'].to_f.nonzero?,
|
|
181
183
|
summary_polyline: polyline,
|
|
182
|
-
source:
|
|
184
|
+
source: 'igpsport',
|
|
183
185
|
}
|
|
184
186
|
end
|
|
185
187
|
|
|
186
188
|
def compute_sensor_summary(records)
|
|
187
189
|
return {} if records.empty?
|
|
188
190
|
|
|
189
|
-
cad = records.map { |r| r[
|
|
190
|
-
pow = records.map { |r| r[
|
|
191
|
-
tmp = records.map { |r| r[
|
|
191
|
+
cad = records.map { |r| r['cadence'] }.compact
|
|
192
|
+
pow = records.map { |r| r['power'] }.compact
|
|
193
|
+
tmp = records.map { |r| r['temperature'] }.compact
|
|
192
194
|
|
|
193
195
|
{
|
|
194
196
|
average_cadence: cad.any? ? (cad.sum.to_f / cad.size).round(1) : nil,
|
|
@@ -196,25 +198,25 @@ module GitFit
|
|
|
196
198
|
average_power: pow.any? ? (pow.sum.to_f / pow.size).round(1) : nil,
|
|
197
199
|
max_power: pow.max,
|
|
198
200
|
average_temperature: tmp.any? ? (tmp.sum.to_f / tmp.size).round(1) : nil,
|
|
199
|
-
calories: nil
|
|
201
|
+
calories: nil,
|
|
200
202
|
}
|
|
201
203
|
end
|
|
202
204
|
|
|
203
205
|
def map_sport(exercise_type)
|
|
204
|
-
EXERCISE_MAP[exercise_type] ||
|
|
206
|
+
EXERCISE_MAP[exercise_type] || 'other'
|
|
205
207
|
end
|
|
206
208
|
|
|
207
209
|
def parse_date(str)
|
|
208
210
|
return nil unless str
|
|
209
|
-
normalized = str.to_s.tr(
|
|
211
|
+
normalized = str.to_s.tr('.', '-')
|
|
210
212
|
Time.parse(normalized)
|
|
211
|
-
rescue
|
|
213
|
+
rescue StandardError
|
|
212
214
|
nil
|
|
213
215
|
end
|
|
214
216
|
|
|
215
217
|
def existing_run_ids
|
|
216
|
-
@db[:activities].where(source:
|
|
217
|
-
.map { |id| id.sub(
|
|
218
|
+
@db[:activities].where(source: 'igpsport').select_map(:run_id)
|
|
219
|
+
.map { |id| id.sub('igpsport_', '') }
|
|
218
220
|
end
|
|
219
221
|
|
|
220
222
|
def raw_path(platform_id)
|