git-fit 0.7.8 → 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 +33 -29
  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 -7
  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 +15 -12
  51. data/lib/git_fit/sync/strava.rb +61 -52
  52. data/lib/git_fit/sync/xingzhe.rb +52 -49
  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,20 +1,22 @@
1
- require_relative "base"
2
- require "faraday"
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 SyncError < RuntimeError; end unless const_defined?(:SyncError)
7
9
 
8
10
  class Strava < Base
9
- BASE_URL = "https://www.strava.com/api/v3"
11
+ BASE_URL = 'https://www.strava.com/api/v3'
10
12
 
11
13
  register_adapter
12
14
  register_config :client_id, :client_secret, :refresh_token
13
15
 
14
16
  def authenticate
15
- return false unless @config["client_id"].to_s != "" &&
16
- @config["client_secret"].to_s != "" &&
17
- @config["refresh_token"].to_s != ""
17
+ return false unless @config['client_id'].to_s != '' &&
18
+ @config['client_secret'].to_s != '' &&
19
+ @config['refresh_token'].to_s != ''
18
20
  @access_token = refresh_access_token
19
21
  @access_token ? true : false
20
22
  end
@@ -27,9 +29,9 @@ module GitFit
27
29
  activities = fetch_activities(@access_token, page)
28
30
  break if activities.empty?
29
31
  activities.each do |act|
30
- act_id = act["id"].to_s
32
+ act_id = act['id'].to_s
31
33
  next if existing.include?(act_id) && raw_file_exists?(act_id)
32
- type = map_sport_type(act["type"], act["sport_type"])
34
+ type = map_sport_type(act['type'], act['sport_type'])
33
35
  next if skip_type?(type)
34
36
  ids << { id: act_id, type: type, summary: act }
35
37
  end
@@ -46,35 +48,41 @@ module GitFit
46
48
  private
47
49
 
48
50
  def refresh_access_token
49
- resp = Faraday.post("https://www.strava.com/oauth/token") do |req|
51
+ resp = Faraday.post('https://www.strava.com/oauth/token') do |req|
50
52
  req.body = {
51
- client_id: @config["client_id"],
52
- client_secret: @config["client_secret"],
53
- refresh_token: @config["refresh_token"],
54
- grant_type: "refresh_token"
53
+ client_id: @config['client_id'],
54
+ client_secret: @config['client_secret'],
55
+ refresh_token: @config['refresh_token'],
56
+ grant_type: 'refresh_token',
55
57
  }
56
58
  end
57
59
  raise SyncError, "Strava token refresh failed: #{resp.status}" unless resp.success?
58
- JSON.parse(resp.body)["access_token"]
60
+ JSON.parse(resp.body)['access_token']
59
61
  end
60
62
 
61
63
  def sync_activity(act, act_id, token)
62
- type = map_sport_type(act["type"], act["sport_type"])
64
+ type = map_sport_type(act['type'], act['sport_type'])
63
65
  return false if skip_type?(type)
64
66
 
65
67
  detail = fetch_detail(act_id, token)
66
68
  return false unless detail
67
69
 
68
- external_id = detail["external_id"]
70
+ external_id = detail['external_id']
69
71
 
70
72
  streams = fetch_streams(act_id, token)
71
73
 
72
- write_source_archive({ source: "strava", detail: detail, streams: streams }, act_id)
74
+ write_source_archive({ source: 'strava', detail: detail, streams: streams }, act_id)
73
75
 
74
76
  l2 = streams_to_l2(detail, streams)
75
77
  write_standardized_json(l2, act_id) if l2
76
78
 
77
- polyline = l2 ? Geo::Polyline.encode(l2.map { |pt| [pt[:positionLat], pt[:positionLong]] }) : act.dig("map", "summary_polyline")
79
+ polyline = if l2
80
+ Geo::Polyline.encode(l2.map do |pt|
81
+ [pt[:positionLat], pt[:positionLong]]
82
+ end)
83
+ else
84
+ act.dig('map', 'summary_polyline')
85
+ end
78
86
 
79
87
  attrs = build_attrs(act, type, polyline, external_id: external_id)
80
88
  upsert_activity(attrs)
@@ -83,7 +91,7 @@ module GitFit
83
91
 
84
92
  def fetch_activities(token, page)
85
93
  resp = Faraday.get("#{BASE_URL}/athlete/activities") do |req|
86
- req.headers["Authorization"] = "Bearer #{token}"
94
+ req.headers['Authorization'] = "Bearer #{token}"
87
95
  req.params = { page: page, per_page: 100 }
88
96
  end
89
97
  return [] unless resp.success?
@@ -92,7 +100,7 @@ module GitFit
92
100
 
93
101
  def fetch_detail(act_id, token)
94
102
  resp = Faraday.get("#{BASE_URL}/activities/#{act_id}") do |req|
95
- req.headers["Authorization"] = "Bearer #{token}"
103
+ req.headers['Authorization'] = "Bearer #{token}"
96
104
  end
97
105
  return nil unless resp.success?
98
106
  JSON.parse(resp.body)
@@ -100,8 +108,9 @@ module GitFit
100
108
 
101
109
  def fetch_streams(act_id, token)
102
110
  resp = Faraday.get("#{BASE_URL}/activities/#{act_id}/streams") do |req|
103
- req.headers["Authorization"] = "Bearer #{token}"
104
- req.params = { keys: "lat_lng,altitude,time,heartrate,cadence,watts,temp,velocity_smooth,distance", key_by_type: true }
111
+ req.headers['Authorization'] = "Bearer #{token}"
112
+ req.params = { keys: 'lat_lng,altitude,time,heartrate,cadence,watts,temp,velocity_smooth,distance',
113
+ key_by_type: true }
105
114
  end
106
115
  return {} unless resp.success?
107
116
  JSON.parse(resp.body)
@@ -117,20 +126,20 @@ module GitFit
117
126
  end
118
127
 
119
128
  def streams_to_l2(detail, streams)
120
- polyline = detail.dig("map", "polyline")
129
+ polyline = detail.dig('map', 'polyline')
121
130
  return nil unless polyline
122
131
 
123
132
  coords = Geo::Polyline.decode(polyline)
124
133
  return nil if coords.empty?
125
134
 
126
- alt_data = streams["altitude"]&.dig("data")
127
- time_data = streams["time"]&.dig("data")
128
- hr_data = streams["heartrate"]&.dig("data")
129
- cad_data = streams["cadence"]&.dig("data")
130
- watts_data = streams["watts"]&.dig("data")
131
- temp_data = streams["temp"]&.dig("data")
132
- speed_data = streams["velocity_smooth"]&.dig("data")
133
- dist_data = streams["distance"]&.dig("data")
135
+ alt_data = streams['altitude']&.dig('data')
136
+ time_data = streams['time']&.dig('data')
137
+ hr_data = streams['heartrate']&.dig('data')
138
+ cad_data = streams['cadence']&.dig('data')
139
+ watts_data = streams['watts']&.dig('data')
140
+ temp_data = streams['temp']&.dig('data')
141
+ speed_data = streams['velocity_smooth']&.dig('data')
142
+ dist_data = streams['distance']&.dig('data')
134
143
 
135
144
  coords.each_with_index.map do |latlng, i|
136
145
  pt = { positionLat: latlng[0], positionLong: latlng[1] }
@@ -147,33 +156,33 @@ module GitFit
147
156
  end
148
157
 
149
158
  def build_attrs(act, category, polyline = nil, external_id: nil)
150
- start_t = Time.parse(act["start_date"]) rescue nil
151
- local_t = Time.parse(act["start_date_local"]) rescue nil
159
+ start_t = Time.parse(act['start_date']) rescue nil
160
+ local_t = Time.parse(act['start_date_local']) rescue nil
152
161
 
153
162
  {
154
163
  run_id: "strava_#{act["id"]}",
155
- name: act["name"],
156
- distance: act["distance"],
157
- moving_time: act["moving_time"],
158
- elapsed_time: act["elapsed_time"],
164
+ name: act['name'],
165
+ distance: act['distance'],
166
+ moving_time: act['moving_time'],
167
+ elapsed_time: act['elapsed_time'],
159
168
  sport_category: category,
160
- sport_type: (act["sport_type"] || act["type"])&.downcase,
169
+ sport_type: (act['sport_type'] || act['type'])&.downcase,
161
170
  start_date: start_t&.iso8601,
162
171
  start_date_local: local_t&.iso8601,
163
- location_country: act["location_country"],
172
+ location_country: act['location_country'],
164
173
  summary_polyline: polyline,
165
- average_heartrate: act["average_heartrate"],
166
- max_heartrate: act["max_heartrate"],
167
- average_cadence: act["average_cadence"],
174
+ average_heartrate: act['average_heartrate'],
175
+ max_heartrate: act['max_heartrate'],
176
+ average_cadence: act['average_cadence'],
168
177
  max_cadence: nil,
169
- average_power: act["average_watts"],
170
- max_power: act["max_watts"],
171
- calories: act["calories"],
172
- average_temperature: act["average_temp"],
173
- average_speed: act["average_speed"],
174
- elevation_gain: act["total_elevation_gain"],
175
- source: "strava",
176
- external_id: external_id
178
+ average_power: act['average_watts'],
179
+ max_power: act['max_watts'],
180
+ calories: act['calories'],
181
+ average_temperature: act['average_temp'],
182
+ average_speed: act['average_speed'],
183
+ elevation_gain: act['total_elevation_gain'],
184
+ source: 'strava',
185
+ external_id: external_id,
177
186
  }
178
187
  end
179
188
 
@@ -186,8 +195,8 @@ module GitFit
186
195
  end
187
196
 
188
197
  def existing_run_ids
189
- @db[:activities].where(source: "strava").select_map(:run_id)
190
- .map { |id| id.sub("strava_", "") }
198
+ @db[:activities].where(source: 'strava').select_map(:run_id)
199
+ .map { |id| id.sub('strava_', '') }
191
200
  end
192
201
  end
193
202
  end
@@ -1,32 +1,35 @@
1
- require_relative "base"
2
- require "fileutils"
3
- require "openssl"
4
- require "tzinfo"
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require 'fileutils'
5
+ require 'openssl'
6
+ require 'tzinfo'
5
7
 
6
8
  module GitFit
7
9
  module Sync
8
10
  class XingZhe < Base
9
- BASE_HOST = "www.imxingzhe.com"
10
- LOGIN_PATH = "/api/v1/user/login/"
11
- WORKOUTS_PATH = "/api/v1/pgworkout/"
12
- STREAM_PATH = "/api/v1/pgworkout/"
13
- DETAIL_PATH = "/api/v1/pgworkout/"
14
- RSA_PUBKEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDmuQkBbijudDAJgfffDeeIButqWHZvUwcRuvWdg89393FSdz3IJUHc0rgI/S3WuU8N0VePJLmVAZtCOK4qe4FY/eKmWpJmn7JfXB4HTMWjPVoyRZmSYjW4L8GrWmh51Qj7DwpTADadF3aq04o+s1b8LXJa8r6+TIqqL5WUHtRqmQIDAQAB"
11
+ BASE_HOST = 'www.imxingzhe.com'
12
+ LOGIN_PATH = '/api/v1/user/login/'
13
+ WORKOUTS_PATH = '/api/v1/pgworkout/'
14
+ STREAM_PATH = '/api/v1/pgworkout/'
15
+ DETAIL_PATH = '/api/v1/pgworkout/'
16
+ RSA_PUBKEY = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDmuQkBbijudDAJgfffDeeIButqWHZvUwcRuvWdg89393FSdz3IJUHc0rgI/S3WuU8N0VePJLmVAZtCOK4qe4FY/eKmWpJmn7JfXB4HTMWjPVoyRZmSYjW4L8GrWmh51Qj7DwpTADadF3aq04o+s1b8LXJa8r6+TIqqL5WUHtRqmQIDAQAB'
15
17
 
16
18
  SPORT_MAP = {
17
- 3 => "ride", 1 => "run", 2 => "hike",
18
- 5 => "swim", 6 => "walk", 11 => "ski", 13 => "workout"
19
+ 3 => 'ride', 1 => 'run', 2 => 'hike',
20
+ 5 => 'swim', 6 => 'walk', 11 => 'ski', 13 => 'workout'
19
21
  }.freeze
20
22
 
21
- RAW_EXT = "json"
23
+ RAW_EXT = 'json'
22
24
 
23
25
  register_adapter
26
+ config_key 'xingzhe'
24
27
  register_config :email, :password
25
28
 
26
29
  def initialize(...)
27
30
  super
28
- @email = @config["email"]
29
- @password = @config["password"]
31
+ @email = @config['email']
32
+ @password = @config['password']
30
33
  end
31
34
 
32
35
  def before_call
@@ -47,9 +50,9 @@ module GitFit
47
50
  acts = fetch_page(offset, limit)
48
51
  break if acts.nil? || acts.empty?
49
52
  acts.each do |act|
50
- aid = act["id"].to_s
53
+ aid = act['id'].to_s
51
54
  next if existing.include?(aid) && raw_file_exists?(aid)
52
- type = map_sport(act["sport"])
55
+ type = map_sport(act['sport'])
53
56
  next if skip_type?(type)
54
57
  ids << { id: aid, type: type, summary: act }
55
58
  end
@@ -87,17 +90,17 @@ module GitFit
87
90
  encrypted = rsa_encrypt(@password)
88
91
  resp = http_request(:post, LOGIN_PATH,
89
92
  body: JSON.generate({ account: @email, password: encrypted }),
90
- headers: { "Content-Type" => "application/json" }
93
+ headers: { 'Content-Type' => 'application/json' }
91
94
  )
92
95
  return false unless resp.code.to_i == 200
93
96
 
94
97
  data = JSON.parse(resp.body)
95
- return false unless data["code"] == 0
98
+ return false unless data['code'] == 0
96
99
 
97
100
  cookie = extract_cookie(resp)
98
101
  return false unless cookie
99
102
 
100
- @headers = { "Cookie" => cookie, "Accept" => "application/json" }
103
+ @headers = { 'Cookie' => cookie, 'Accept' => 'application/json' }
101
104
  true
102
105
  end
103
106
 
@@ -108,7 +111,7 @@ module GitFit
108
111
  end
109
112
 
110
113
  def extract_cookie(resp)
111
- set_cookie = resp["set-cookie"]
114
+ set_cookie = resp['set-cookie']
112
115
  return nil unless set_cookie
113
116
  m = set_cookie.match(/sessionid=([^;]+)/)
114
117
  m ? "sessionid=#{m[1]}" : nil
@@ -125,7 +128,7 @@ module GitFit
125
128
  )
126
129
  return nil unless resp.code.to_i == 200
127
130
 
128
- JSON.parse(resp.body).dig("data", "data")
131
+ JSON.parse(resp.body).dig('data', 'data')
129
132
  end
130
133
 
131
134
  def tz_resolver
@@ -141,48 +144,48 @@ module GitFit
141
144
  tz_string = tz_resolver.resolve(lat, lng)
142
145
  tz = TZInfo::Timezone.get(tz_string)
143
146
  tz.utc_to_local(start_utc).iso8601
144
- rescue => e
147
+ rescue StandardError => e
145
148
  warn "XingZhe: timezone resolution failed: #{e.message}"
146
149
  start_utc.iso8601
147
150
  end
148
151
 
149
152
  def process_activity(act, type)
150
- aid = act["id"].to_s
151
- start_t = Time.at(act["start_time"] / 1000.0).utc rescue nil
153
+ aid = act['id'].to_s
154
+ start_t = Time.at(act['start_time'] / 1000.0).utc rescue nil
152
155
  return false unless start_t
153
156
 
154
157
  attrs = {
155
158
  run_id: "xingzhe_#{aid}",
156
- name: act["title"] || "XingZhe Activity",
157
- distance: act["distance"].to_f,
158
- moving_time: (t = act["duration"].to_i) > 0 ? t : nil,
159
+ name: act['title'] || 'XingZhe Activity',
160
+ distance: act['distance'].to_f,
161
+ moving_time: (t = act['duration'].to_i) > 0 ? t : nil,
159
162
  elapsed_time: t > 0 ? t : nil,
160
163
  sport_category: type,
161
- sport_type: act["sport"].to_s,
164
+ sport_type: act['sport'].to_s,
162
165
  start_date: start_t.iso8601,
163
166
  start_date_local: start_t.iso8601,
164
- average_speed: (act["avg_speed"].to_f / 3.6).round(2).nonzero?,
165
- elevation_gain: act["elevation_gain"].to_f.nonzero?,
166
- source: "xingzhe"
167
+ average_speed: (act['avg_speed'].to_f / 3.6).round(2).nonzero?,
168
+ elevation_gain: act['elevation_gain'].to_f.nonzero?,
169
+ source: 'xingzhe',
167
170
  }
168
171
 
169
172
  pts = nil
170
173
  stream = fetch_stream(aid)
171
174
  if stream
172
175
  write_source_archive({
173
- source: "xingzhe",
174
- fetchedAt: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
175
- stream: stream
176
+ source: 'xingzhe',
177
+ fetchedAt: Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ'),
178
+ stream: stream,
176
179
  }, aid)
177
- if (locs = stream["location"]) && locs.size >= 2
180
+ if (locs = stream['location']) && locs.size >= 2
178
181
  pts = locs.map { |lng, lat| [lat, lng] }
179
182
  write_std(pts, stream, aid)
180
183
  attrs[:summary_polyline] = stream_to_polyline(aid)
181
184
  end
182
185
  end
183
186
 
184
- if attrs[:summary_polyline].nil? && (detail = fetch_activity_detail(aid)) && (segments = detail["segments_km"])
185
- pts = segments.map { |s| [s["latitude"], s["longitude"]] }
187
+ if attrs[:summary_polyline].nil? && (detail = fetch_activity_detail(aid)) && (segments = detail['segments_km'])
188
+ pts = segments.map { |s| [s['latitude'], s['longitude']] }
186
189
  attrs[:summary_polyline] = Geo::Polyline.encode(pts) if pts.size >= 2
187
190
  end
188
191
 
@@ -200,7 +203,7 @@ module GitFit
200
203
  resp = http_request(:get, "#{STREAM_PATH}#{id}/stream/", headers: @headers || {})
201
204
  return nil unless resp.code.to_i == 200
202
205
 
203
- JSON.parse(resp.body).dig("data")
206
+ JSON.parse(resp.body).dig('data')
204
207
  rescue JSON::ParserError
205
208
  nil
206
209
  end
@@ -208,17 +211,17 @@ module GitFit
208
211
  def write_std(pts, stream, id)
209
212
  dir = std_dir
210
213
  FileUtils.mkdir_p(dir)
211
- ts_arr = stream["timestamp"] || []
212
- alt_arr = stream["altitude"] || []
213
- spd_arr = stream["speed"] || []
214
- dst_arr = stream["distance"] || []
214
+ ts_arr = stream['timestamp'] || []
215
+ alt_arr = stream['altitude'] || []
216
+ spd_arr = stream['speed'] || []
217
+ dst_arr = stream['distance'] || []
215
218
 
216
219
  extended = pts.each_with_index.map do |(lat, lng), i|
217
220
  ts_val = ts_arr[i]
218
221
  {
219
222
  latitude: lat, longitude: lng,
220
223
  altitude: alt_arr[i],
221
- timestamp: ts_val ? Time.at(ts_val).utc.strftime("%Y-%m-%dT%H:%M:%SZ") : nil,
224
+ timestamp: ts_val ? Time.at(ts_val).utc.strftime('%Y-%m-%dT%H:%M:%SZ') : nil,
222
225
  speed: spd_arr[i],
223
226
  distance: dst_arr[i]
224
227
  }
@@ -232,7 +235,7 @@ module GitFit
232
235
 
233
236
  def stream_to_polyline(id)
234
237
  raw = JSON.parse(File.read(raw_path(id)))
235
- locs = raw.dig("stream", "location") || []
238
+ locs = raw.dig('stream', 'location') || []
236
239
  return nil if locs.size < 2
237
240
 
238
241
  pts = locs.map { |lng, lat| [lat, lng] }
@@ -246,16 +249,16 @@ module GitFit
246
249
  return nil unless resp.code.to_i == 200
247
250
 
248
251
  data = JSON.parse(resp.body)
249
- data.dig("data", "workout")
252
+ data.dig('data', 'workout')
250
253
  end
251
254
 
252
255
  def map_sport(sport)
253
- SPORT_MAP[sport] || "other"
256
+ SPORT_MAP[sport] || 'other'
254
257
  end
255
258
 
256
259
  def existing_run_ids
257
- @db[:activities].where(source: "xingzhe").select_map(:run_id)
258
- .map { |id| id.sub("xingzhe_", "") }
260
+ @db[:activities].where(source: 'xingzhe').select_map(:run_id)
261
+ .map { |id| id.sub('xingzhe_', '') }
259
262
  end
260
263
  end
261
264
  end