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.
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 +29 -27
  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 -6
  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 +14 -11
  51. data/lib/git_fit/sync/strava.rb +61 -52
  52. data/lib/git_fit/sync/xingzhe.rb +52 -50
  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,10 +1,12 @@
1
- require "digest"
2
- require "fileutils"
3
- require "json"
4
- require "nokogiri"
5
- require "time"
6
- require "tzinfo"
7
- require "zip"
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'fileutils'
5
+ require 'json'
6
+ require 'nokogiri'
7
+ require 'time'
8
+ require 'tzinfo'
9
+ require 'zip'
8
10
 
9
11
  module GitFit
10
12
  module Import
@@ -16,7 +18,7 @@ module GitFit
16
18
 
17
19
  APPLE_WATCH_PATTERN = /Apple Watch/
18
20
 
19
- DEFAULT_ZIP_PATH = "data/import/apple_health/export.zip"
21
+ DEFAULT_ZIP_PATH = 'data/import/apple_health/export.zip'
20
22
 
21
23
  HeartRateRecord = Struct.new(:time, :value, keyword_init: true)
22
24
 
@@ -24,7 +26,7 @@ module GitFit
24
26
 
25
27
  def initialize(config:, db:, activity_filter: nil, privacy: nil, time_budget: nil)
26
28
  super
27
- @zip_path = @config["zip_path"] || DEFAULT_ZIP_PATH
29
+ @zip_path = @config['zip_path'] || DEFAULT_ZIP_PATH
28
30
  @heart_rate_index = nil
29
31
  @heart_rate_by_date = nil
30
32
  @skip_patterns = load_skip_sources
@@ -64,40 +66,40 @@ module GitFit
64
66
  hr_records = []
65
67
  @route_index = []
66
68
  zip = Zip::File.new(@zip_path)
67
- xml_entry = zip.find_entry("apple_health_export/export.xml")
69
+ xml_entry = zip.find_entry('apple_health_export/export.xml')
68
70
  return results unless xml_entry
69
71
 
70
72
  io = xml_entry.get_input_stream
71
- buffer = ""
73
+ buffer = ''
72
74
  in_workout = false
73
75
  route_buf = nil
74
76
 
75
77
  io.each_line do |line|
76
- if line.include?("HKQuantityTypeIdentifierHeartRate") && APPLE_WATCH_PATTERN.match?(line)
78
+ if line.include?('HKQuantityTypeIdentifierHeartRate') && APPLE_WATCH_PATTERN.match?(line)
77
79
  hr = parse_hr_line(line)
78
80
  hr_records << hr if hr
79
81
  end
80
82
 
81
- if line.include?("<WorkoutRoute ")
83
+ if line.include?('<WorkoutRoute ')
82
84
  route_buf = line
83
85
  elsif route_buf
84
86
  route_buf << line
85
- if line.include?("</WorkoutRoute>")
87
+ if line.include?('</WorkoutRoute>')
86
88
  process_route_entry(route_buf)
87
89
  route_buf = nil
88
90
  end
89
91
  end
90
92
 
91
93
  if !in_workout
92
- if line.include?("<Workout ")
94
+ if line.include?('<Workout ')
93
95
  in_workout = true
94
96
  buffer = line
95
97
  end
96
98
  else
97
99
  buffer << line
98
- if line.include?("</Workout>")
100
+ if line.include?('</Workout>')
99
101
  process_workout_chunk(buffer, existing, results)
100
- buffer = ""
102
+ buffer = ''
101
103
  in_workout = false
102
104
  end
103
105
  end
@@ -107,7 +109,7 @@ module GitFit
107
109
 
108
110
  @heart_rate_by_date = {}
109
111
  @heart_rate_index.each do |hr|
110
- date_key = hr.time.strftime("%Y-%m-%d")
112
+ date_key = hr.time.strftime('%Y-%m-%d')
111
113
  @heart_rate_by_date[date_key] ||= []
112
114
  @heart_rate_by_date[date_key] << hr
113
115
  end
@@ -153,7 +155,7 @@ module GitFit
153
155
  begin
154
156
  zip = Zip::File.new(@zip_path)
155
157
  route_paths.each do |gpx_path|
156
- normalized = gpx_path.sub(%r{^/?}, "apple_health_export/")
158
+ normalized = gpx_path.sub(%r{^/?}, 'apple_health_export/')
157
159
  entry = zip.find_entry(normalized)
158
160
  next unless entry
159
161
  data = entry.get_input_stream.read
@@ -162,7 +164,7 @@ module GitFit
162
164
  File.rename(tmpp, gpx_l1_path)
163
165
  break
164
166
  end
165
- rescue => e
167
+ rescue StandardError => e
166
168
  $stderr.puts "AppleHealth: GPX extract error for #{run_id}: #{e.message}"
167
169
  ensure
168
170
  zip&.close
@@ -210,15 +212,15 @@ module GitFit
210
212
  end
211
213
 
212
214
  def source_label
213
- "AppleHealth"
215
+ 'AppleHealth'
214
216
  end
215
217
 
216
218
  def raw_dir
217
- File.join("data", "raw", "apple_health")
219
+ File.join('data', 'raw', 'apple_health')
218
220
  end
219
221
 
220
222
  def std_dir
221
- File.join("data", "std", "apple_health")
223
+ File.join('data', 'std', 'apple_health')
222
224
  end
223
225
 
224
226
  def raw_path(platform_id)
@@ -233,10 +235,10 @@ module GitFit
233
235
  end
234
236
 
235
237
  def load_skip_sources
236
- configured = @config["skip_sources"]
238
+ configured = @config['skip_sources']
237
239
  return [] unless configured
238
240
 
239
- list = configured.is_a?(String) ? configured.split(",").map(&:strip) : configured
241
+ list = configured.is_a?(String) ? configured.split(',').map(&:strip) : configured
240
242
  validated = list.select do |name|
241
243
  SKIPPED_SOURCE_PATTERNS.any? { |p| p.match?(name) }
242
244
  end
@@ -247,7 +249,7 @@ module GitFit
247
249
  end
248
250
 
249
251
  def existing_run_ids_set
250
- @db[:activities].where(source: "apple_health").select_map(:run_id).map(&:to_s).to_set
252
+ @db[:activities].where(source: 'apple_health').select_map(:run_id).map(&:to_s).to_set
251
253
  end
252
254
 
253
255
  def parse_hr_line(line)
@@ -277,7 +279,7 @@ module GitFit
277
279
  if @heart_rate_by_date
278
280
  results = []
279
281
  (start_t.to_date..end_t.to_date).each do |date|
280
- date_key = date.strftime("%Y-%m-%d")
282
+ date_key = date.strftime('%Y-%m-%d')
281
283
  bucket = @heart_rate_by_date[date_key]
282
284
  if bucket&.any?
283
285
  lo = bucket.bsearch_index { |hr| hr.time >= start_t }
@@ -301,24 +303,24 @@ module GitFit
301
303
 
302
304
  def process_workout_chunk(raw_xml, existing_ids, results)
303
305
  doc = Nokogiri::XML("<root>#{raw_xml}</root>")
304
- workout = doc.at_xpath("//Workout")
306
+ workout = doc.at_xpath('//Workout')
305
307
  return unless workout
306
308
 
307
- source_name = workout["sourceName"]
309
+ source_name = workout['sourceName']
308
310
  return if known_source?(source_name)
309
311
 
310
- workout_type = workout["workoutActivityType"]
312
+ workout_type = workout['workoutActivityType']
311
313
  return unless workout_type
312
314
 
313
- start_date = workout["startDate"]
314
- end_date = workout["endDate"]
315
+ start_date = workout['startDate']
316
+ end_date = workout['endDate']
315
317
  return if start_date.nil? || end_date.nil?
316
318
 
317
- duration = workout["duration"]
318
- duration_unit = workout["durationUnit"] || "min"
319
+ duration = workout['duration']
320
+ duration_unit = workout['durationUnit'] || 'min'
319
321
  return if duration.nil?
320
322
 
321
- creation_date = workout["creationDate"] || start_date
323
+ creation_date = workout['creationDate'] || start_date
322
324
  run_id = generate_run_id(source_name, creation_date, start_date, end_date)
323
325
  return if existing_ids.include?("apple_health_#{run_id}")
324
326
 
@@ -329,7 +331,7 @@ module GitFit
329
331
  meta = extract_metadata(workout)
330
332
  route_paths = extract_route_paths(workout)
331
333
  route_paths = match_routes_by_time(start_date, end_date) if route_paths.empty?
332
- source_version = workout["sourceVersion"]
334
+ source_version = workout['sourceVersion']
333
335
 
334
336
  results << {
335
337
  id: run_id,
@@ -346,26 +348,26 @@ module GitFit
346
348
  stats: stats,
347
349
  meta: meta,
348
350
  route_paths: route_paths,
349
- creation_date: creation_date
351
+ creation_date: creation_date,
350
352
  }
351
353
  end
352
354
 
353
355
  def process_route_entry(xml)
354
356
  doc = Nokogiri::XML("<root>#{xml}</root>")
355
- route = doc.at_xpath("//WorkoutRoute")
357
+ route = doc.at_xpath('//WorkoutRoute')
356
358
  return unless route
357
359
 
358
- start_date = route["startDate"]
359
- end_date = route["endDate"]
360
+ start_date = route['startDate']
361
+ end_date = route['endDate']
360
362
  return unless start_date && end_date
361
363
 
362
- paths = route.xpath("FileReference").filter_map { |ref| ref["path"] }
364
+ paths = route.xpath('FileReference').filter_map { |ref| ref['path'] }
363
365
  return if paths.empty?
364
366
 
365
367
  @route_index << {
366
368
  route_start: (Time.parse(start_date) rescue nil),
367
369
  route_end: (Time.parse(end_date) rescue nil),
368
- route_paths: paths
370
+ route_paths: paths,
369
371
  }
370
372
  end
371
373
 
@@ -386,47 +388,47 @@ module GitFit
386
388
 
387
389
  def extract_statistics(workout)
388
390
  stats = {}
389
- workout.xpath("WorkoutStatistics").each do |stat|
390
- type = stat["type"]
391
- sum = stat["sum"]
392
- unit = stat["unit"]
391
+ workout.xpath('WorkoutStatistics').each do |stat|
392
+ type = stat['type']
393
+ sum = stat['sum']
394
+ unit = stat['unit']
393
395
  next unless type && sum
394
- stats[type] = { "sum" => sum, "unit" => unit }
396
+ stats[type] = { 'sum' => sum, 'unit' => unit }
395
397
  end
396
398
  stats
397
399
  end
398
400
 
399
401
  def extract_metadata(workout)
400
402
  meta = {}
401
- workout.xpath("MetadataEntry").each do |entry|
402
- key = entry["key"]
403
- value = entry["value"]
403
+ workout.xpath('MetadataEntry').each do |entry|
404
+ key = entry['key']
405
+ value = entry['value']
404
406
  meta[key] = value if key && value
405
407
  end
406
408
  meta
407
409
  end
408
410
 
409
411
  def extract_route_paths(workout)
410
- workout.xpath("WorkoutRoute/FileReference").filter_map do |ref|
411
- ref["path"]
412
+ workout.xpath('WorkoutRoute/FileReference').filter_map do |ref|
413
+ ref['path']
412
414
  end
413
415
  end
414
416
 
415
417
  def parse_gpx_file(file_path)
416
418
  doc = Nokogiri::XML(File.read(file_path))
417
419
  doc.remove_namespaces!
418
- doc.xpath("//trkpt").filter_map do |pt|
419
- lat = pt["lat"]&.to_f
420
- lng = pt["lon"]&.to_f
420
+ doc.xpath('//trkpt').filter_map do |pt|
421
+ lat = pt['lat']&.to_f
422
+ lng = pt['lon']&.to_f
421
423
  next unless lat && lng
422
- ele = pt.at_xpath("ele")&.text&.to_f
423
- time_str = pt.at_xpath("time")&.text
424
- ext = pt.at_xpath("extensions")
425
- speed = ext&.at_xpath("speed")&.text&.to_f
424
+ ele = pt.at_xpath('ele')&.text&.to_f
425
+ time_str = pt.at_xpath('time')&.text
426
+ ext = pt.at_xpath('extensions')
427
+ speed = ext&.at_xpath('speed')&.text&.to_f
426
428
  speed = nil if speed && speed < MIN_GPX_SPEED
427
429
  { lat: lat, lng: lng, ele: ele, time: time_str, speed: speed }
428
430
  end
429
- rescue => e
431
+ rescue StandardError => e
430
432
  $stderr.puts "AppleHealth: GPX parse error for #{file_path}: #{e.message}"
431
433
  nil
432
434
  end
@@ -439,10 +441,10 @@ module GitFit
439
441
 
440
442
  cals_sum = 0.0
441
443
  stats.each do |type_str, data|
442
- if type_str.include?("Distance")
443
- distance = parse_distance(data["sum"], data["unit"])
444
- elsif type_str.include?("ActiveEnergyBurned") || type_str.include?("BasalEnergyBurned")
445
- cals_sum += data["sum"].to_f
444
+ if type_str.include?('Distance')
445
+ distance = parse_distance(data['sum'], data['unit'])
446
+ elsif type_str.include?('ActiveEnergyBurned') || type_str.include?('BasalEnergyBurned')
447
+ cals_sum += data['sum'].to_f
446
448
  end
447
449
  end
448
450
  calories = cals_sum > 0 ? cals_sum.round : nil
@@ -462,16 +464,16 @@ module GitFit
462
464
  sport_category: map_sport_type(workout_type),
463
465
  sport_type: workout_type,
464
466
  start_date: format_datetime(start_date),
465
- start_date_local: meta["HKTimeZone"] ? format_datetime(start_date, utc: false) : nil,
467
+ start_date_local: meta['HKTimeZone'] ? format_datetime(start_date, utc: false) : nil,
466
468
  summary_polyline: polyline,
467
469
  average_heartrate: hr_avg,
468
470
  max_heartrate: hr_max,
469
471
  average_speed: avg_speed,
470
472
  calories: calories,
471
473
  elevation_gain: elevation_gain&.round(1),
472
- average_temperature: parse_temperature(meta["HKWeatherTemperature"]),
473
- source: "apple_health",
474
- external_id: meta["HKExternalUUID"]
474
+ average_temperature: parse_temperature(meta['HKWeatherTemperature']),
475
+ source: 'apple_health',
476
+ external_id: meta['HKExternalUUID'],
475
477
  }
476
478
  end
477
479
 
@@ -493,9 +495,9 @@ module GitFit
493
495
  def parse_duration(value, unit)
494
496
  v = value.to_f
495
497
  case unit.to_s.downcase
496
- when "min" then (v * 60).round
497
- when "sec" then v.round
498
- when "hour" then (v * 3600).round
498
+ when 'min' then (v * 60).round
499
+ when 'sec' then v.round
500
+ when 'hour' then (v * 3600).round
499
501
  else (v * 60).round
500
502
  end
501
503
  end
@@ -503,10 +505,10 @@ module GitFit
503
505
  def parse_distance(value, unit)
504
506
  v = value.to_f
505
507
  case unit.to_s.downcase
506
- when "km" then (v * 1000).round(2)
507
- when "mi" then (v * 1609.344).round(2)
508
- when "m" then v.round(2)
509
- when "ft" then (v * 0.3048).round(2)
508
+ when 'km' then (v * 1000).round(2)
509
+ when 'mi' then (v * 1609.344).round(2)
510
+ when 'm' then v.round(2)
511
+ when 'ft' then (v * 0.3048).round(2)
510
512
  else v.round(2)
511
513
  end
512
514
  end
@@ -517,7 +519,7 @@ module GitFit
517
519
  return nil unless m
518
520
  num = m[1].to_f
519
521
  unit = m[2].downcase
520
- if unit.start_with?("degf") || unit.start_with?("°f")
522
+ if unit.start_with?('degf') || unit.start_with?('°f')
521
523
  ((num - 32) * 5.0 / 9.0).round(1)
522
524
  else
523
525
  num.round(1)
@@ -536,22 +538,22 @@ module GitFit
536
538
  def normalize_date(date_str)
537
539
  t = Time.parse(date_str) rescue nil
538
540
  return date_str unless t
539
- t.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
541
+ t.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
540
542
  end
541
543
 
542
544
  def map_sport_type(workout_type)
543
- return "other" unless workout_type
544
- key = workout_type.sub(/\AHKWorkoutActivityType/, "")
545
- return "workout" if key.downcase.include?("strength")
545
+ return 'other' unless workout_type
546
+ key = workout_type.sub(/\AHKWorkoutActivityType/, '')
547
+ return 'workout' if key.downcase.include?('strength')
546
548
  GitFit::SportMapper.canonicalize(key)
547
549
  end
548
550
 
549
551
  def display_name(workout_type, _start_date)
550
- workout_type.sub(/\AHKWorkoutActivityType/, "")
552
+ workout_type.sub(/\AHKWorkoutActivityType/, '')
551
553
  end
552
554
 
553
555
  def build_title(workout_type, start_date)
554
- label = workout_type.sub(/\AHKWorkoutActivityType/, "")
556
+ label = workout_type.sub(/\AHKWorkoutActivityType/, '')
555
557
  date = start_date ? start_date[0..9] : nil
556
558
  date ? "#{date} · #{label}" : label
557
559
  end
@@ -1,23 +1,26 @@
1
- require "digest"
2
- require "fileutils"
3
- require "json"
4
- require "nokogiri"
5
- require "tzinfo"
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'fileutils'
5
+ require 'json'
6
+ require 'nokogiri'
7
+ require 'tzinfo'
6
8
 
7
9
  module GitFit
8
10
  module Import
9
11
  class LocalFile < Source::Base
10
12
  IMPORT_DIRS = {
11
- "gpx" => "data/import/gpx",
12
- "tcx" => "data/import/tcx",
13
- "fit" => "data/import/fit"
13
+ 'gpx' => 'data/import/gpx',
14
+ 'tcx' => 'data/import/tcx',
15
+ 'fit' => 'data/import/fit',
14
16
  }.freeze
15
17
 
16
18
  SUPPORTED_EXTENSIONS = %w[.gpx .tcx .fit].freeze
17
19
 
18
- NS_TCX = { tcx: "http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2" }.freeze
20
+ NS_TCX = { tcx: 'http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2' }.freeze
19
21
 
20
- def initialize(config:, db:, activity_filter: nil, privacy: nil, time_budget: nil, sources: nil, sport_override: nil)
22
+ def initialize(config:, db:, activity_filter: nil, privacy: nil, time_budget: nil, sources: nil,
23
+ sport_override: nil)
21
24
  super(config:, db:, activity_filter:, privacy:, time_budget:)
22
25
  @sources = sources
23
26
  @sport_override = sport_override
@@ -32,7 +35,7 @@ module GitFit
32
35
  dir = IMPORT_DIRS[source]
33
36
  next unless dir && File.directory?(dir)
34
37
 
35
- files = Dir.glob(File.join(dir, "*"), File::FNM_CASEFOLD)
38
+ files = Dir.glob(File.join(dir, '*'), File::FNM_CASEFOLD)
36
39
  .select { |f| SUPPORTED_EXTENSIONS.include?(File.extname(f).downcase) && File.file?(f) }
37
40
 
38
41
  total_imported += process_files(files, source)
@@ -50,15 +53,15 @@ module GitFit
50
53
  end
51
54
 
52
55
  def source_label
53
- "LocalFile"
56
+ 'LocalFile'
54
57
  end
55
58
 
56
59
  def raw_dir_for(source)
57
- File.join("data", "raw", source)
60
+ File.join('data', 'raw', source)
58
61
  end
59
62
 
60
63
  def std_dir_for(source)
61
- File.join("data", "std", source)
64
+ File.join('data', 'std', source)
62
65
  end
63
66
 
64
67
  private
@@ -107,7 +110,7 @@ module GitFit
107
110
  activity[:source] = source
108
111
  activity[:run_id] = imported.zero? ? platform_id : "#{platform_id}_#{imported}"
109
112
 
110
- if source == "gpx" && @sport_override
113
+ if source == 'gpx' && @sport_override
111
114
  activity[:sport_category] = SportMapper.canonicalize(@sport_override)
112
115
  end
113
116
 
@@ -141,18 +144,18 @@ module GitFit
141
144
  end
142
145
 
143
146
  { activity_count: imported }
144
- rescue => e
147
+ rescue StandardError => e
145
148
  $stderr.puts "LocalFile: error importing #{file_path}: #{e.message}"
146
149
  nil
147
150
  end
148
151
 
149
152
  def parse_file(ext, content, file_path)
150
153
  case ext
151
- when ".gpx"
154
+ when '.gpx'
152
155
  GitFit::Parser::GPX.new.call(content)
153
- when ".tcx"
156
+ when '.tcx'
154
157
  GitFit::Parser::TCX.new.call(content)
155
- when ".fit"
158
+ when '.fit'
156
159
  GitFit::Parser::FIT.new.call(file_path)
157
160
  else
158
161
  []
@@ -161,11 +164,11 @@ module GitFit
161
164
 
162
165
  def extract_l2_points(ext, content, file_path)
163
166
  case ext
164
- when ".gpx"
167
+ when '.gpx'
165
168
  extract_gpx_points(content)
166
- when ".tcx"
169
+ when '.tcx'
167
170
  extract_tcx_points(content)
168
- when ".fit"
171
+ when '.fit'
169
172
  extract_fit_points(file_path)
170
173
  else
171
174
  nil
@@ -175,36 +178,36 @@ module GitFit
175
178
  def extract_gpx_points(xml)
176
179
  doc = Nokogiri::XML(xml)
177
180
  doc.remove_namespaces!
178
- doc.xpath("//trkpt").filter_map do |pt|
179
- lat = pt["lat"]&.to_f
180
- lon = pt["lon"]&.to_f
181
+ doc.xpath('//trkpt').filter_map do |pt|
182
+ lat = pt['lat']&.to_f
183
+ lon = pt['lon']&.to_f
181
184
  next unless lat && lon
182
- ele = pt.at_xpath("ele")&.text&.to_f
183
- time_str = pt.at_xpath("time")&.text
185
+ ele = pt.at_xpath('ele')&.text&.to_f
186
+ time_str = pt.at_xpath('time')&.text
184
187
  time = parse_timestamp(time_str)
185
- hr = pt.at_xpath("extensions//hr")&.text&.to_f
188
+ hr = pt.at_xpath('extensions//hr')&.text&.to_f
186
189
  [lat, lon, ele, time, hr]
187
190
  end
188
- rescue => e
191
+ rescue StandardError => e
189
192
  $stderr.puts "LocalFile: GPX parse error: #{e.message}"
190
193
  nil
191
194
  end
192
195
 
193
196
  def extract_tcx_points(xml)
194
197
  doc = Nokogiri::XML(xml)
195
- doc.xpath("//tcx:Trackpoint", NS_TCX).filter_map do |tp|
196
- pos = tp.at_xpath("tcx:Position", NS_TCX)
198
+ doc.xpath('//tcx:Trackpoint', NS_TCX).filter_map do |tp|
199
+ pos = tp.at_xpath('tcx:Position', NS_TCX)
197
200
  next unless pos
198
- lat = pos.at_xpath("tcx:LatitudeDegrees", NS_TCX)&.text&.to_f
199
- lon = pos.at_xpath("tcx:LongitudeDegrees", NS_TCX)&.text&.to_f
201
+ lat = pos.at_xpath('tcx:LatitudeDegrees', NS_TCX)&.text&.to_f
202
+ lon = pos.at_xpath('tcx:LongitudeDegrees', NS_TCX)&.text&.to_f
200
203
  next unless lat && lon
201
- ele = tp.at_xpath("tcx:AltitudeMeters", NS_TCX)&.text&.to_f
202
- time_str = tp.at_xpath("tcx:Time", NS_TCX)&.text
204
+ ele = tp.at_xpath('tcx:AltitudeMeters', NS_TCX)&.text&.to_f
205
+ time_str = tp.at_xpath('tcx:Time', NS_TCX)&.text
203
206
  time = parse_timestamp(time_str)
204
- hr = tp.at_xpath("tcx:HeartRateBpm/tcx:Value", NS_TCX)&.text&.to_f
207
+ hr = tp.at_xpath('tcx:HeartRateBpm/tcx:Value', NS_TCX)&.text&.to_f
205
208
  [lat, lon, ele, time, hr]
206
209
  end
207
- rescue => e
210
+ rescue StandardError => e
208
211
  $stderr.puts "LocalFile: TCX parse error: #{e.message}"
209
212
  nil
210
213
  end
@@ -212,14 +215,14 @@ module GitFit
212
215
  def extract_fit_points(file_path)
213
216
  records = GitFit::FIT::Decoder.decode(file_path)
214
217
  records.filter_map do |r|
215
- lat = r["positionLat"]
216
- lon = r["positionLong"]
218
+ lat = r['positionLat']
219
+ lon = r['positionLong']
217
220
  next unless lat && lon
218
- ts = r["timestamp"]
221
+ ts = r['timestamp']
219
222
  time = ts.is_a?(Numeric) ? (Time.utc(1989, 12, 31) + ts) : (Time.parse(ts.to_s) rescue nil)
220
- [lat, lon, r["altitude"], time, r["heartRate"]]
223
+ [lat, lon, r['altitude'], time, r['heartRate']]
221
224
  end
222
- rescue => e
225
+ rescue StandardError => e
223
226
  $stderr.puts "LocalFile: FIT decode error: #{e.message}"
224
227
  nil
225
228
  end
@@ -1,2 +1,4 @@
1
- require_relative "import/local_file"
2
- require_relative "import/apple_health"
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'import/local_file'
4
+ require_relative 'import/apple_health'
@@ -1,10 +1,12 @@
1
- require "erb"
2
- require "digest"
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'digest'
3
5
 
4
6
  module GitFit
5
7
  module Install
6
8
  module Actions
7
- TEMPLATE_DIR = File.expand_path("actions", __dir__)
9
+ TEMPLATE_DIR = File.expand_path('actions', __dir__)
8
10
  VERSION_STAMP = "# Generated by git-fit v#{GitFit::VERSION} — do not edit manually\n"
9
11
 
10
12
  def self.render(name, db_path:)
@@ -20,11 +22,11 @@ module GitFit
20
22
 
21
23
  def self.install_family(family, check: false, dry_run: false)
22
24
  dir = File.join(TEMPLATE_DIR, family)
23
- db_path = ENV["GIT_FIT_DATABASE_PATH"] || "db/fit.sqlite3"
25
+ db_path = ENV['GIT_FIT_DATABASE_PATH'] || 'db/fit.sqlite3'
24
26
  results = []
25
27
 
26
- Dir.glob("*.yml.erb", base: dir).sort.map do |tpl|
27
- action_name = tpl.delete_suffix(".yml.erb")
28
+ Dir.glob('*.yml.erb', base: dir).sort.map do |tpl|
29
+ action_name = tpl.delete_suffix('.yml.erb')
28
30
  content = render("#{family}/#{action_name}", db_path: db_path)
29
31
  stamped = "#{VERSION_STAMP}\n#{content}"
30
32
  out_path = ".github/actions/#{family}/#{action_name}/action.yml"
@@ -61,9 +63,9 @@ module GitFit
61
63
  end
62
64
 
63
65
  def self.diff(a, b)
64
- require "tempfile"
65
- ta = Tempfile.new("a")
66
- tb = Tempfile.new("b")
66
+ require 'tempfile'
67
+ ta = Tempfile.new('a')
68
+ tb = Tempfile.new('b')
67
69
  ta.write(a)
68
70
  tb.write(b)
69
71
  ta.close
@@ -76,14 +78,14 @@ module GitFit
76
78
  families.flat_map { |f| install_family(f, check: check, dry_run: dry_run) }
77
79
  end
78
80
 
79
- def self.restore_action(db_path = "db/fit.sqlite3")
80
- render("db-store/restore", db_path: db_path)
81
+ def self.restore_action(db_path = 'db/fit.sqlite3')
82
+ render('db-store/restore', db_path: db_path)
81
83
  end
82
84
 
83
85
  RESTORE_ACTION = restore_action
84
86
 
85
- def self.save_action(db_path = "db/fit.sqlite3")
86
- render("db-store/save", db_path: db_path)
87
+ def self.save_action(db_path = 'db/fit.sqlite3')
88
+ render('db-store/save', db_path: db_path)
87
89
  end
88
90
 
89
91
  SAVE_ACTION = save_action