git-fit 0.3.2 → 0.4.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 66005a7ca77d10a5922f57d085f4354c3ec4b0838faf350b9b2533e1159b7870
4
- data.tar.gz: 783823d2e2e2b8031572527afdaaa8a5b2b06491b9a98f07671147233609c315
3
+ metadata.gz: a3c36d045a4ed37a0eaad4f219367f1d514af31c26bcae72e76cbeec7bb5dbb0
4
+ data.tar.gz: ab845ce910206dae48a131315427bf06604bd02988b097eb462ae5805727ab97
5
5
  SHA512:
6
- metadata.gz: c89175b5dc5c16ac6def3cf27bb5a9ed3019f3bd0eeac32667948e36449eb3f3f2cf398a76106314be790587233396da65c7f278665c6de92284597d6870f6c4
7
- data.tar.gz: 6de6c3ed115ce17df85754c0c2d90e8ce069fc91e86593ade7061ee64acdeb3661903667680fb26a4604a4eadf57c5c9b7f8832b3bdd5648d7b15ca0aebd8091
6
+ metadata.gz: 9822b621a741ce52c68c6510f6f73a1da52ae544c4371d05ffe430cacd1d882a914f4bd6959b98add49a13595b0ffc703b4258fd5dd5a6535ad887117d5e8be0
7
+ data.tar.gz: 139da861fd411de27553e13e03ec0b3c7d94e5019c64e3e6ba820415e49f87f43ed58490565c408801554f30c3b0a2fa03133e48c4d783ffa7276b54e9f67e22
data/lib/git-fit.rb CHANGED
@@ -51,11 +51,14 @@ require_relative "git_fit/sport_mapper"
51
51
  require_relative "git_fit/geo"
52
52
  require_relative "git_fit/fit"
53
53
  require_relative "git_fit/parser"
54
+ require_relative "git_fit/source"
54
55
  require_relative "git_fit/timezone/resolver"
55
56
  require_relative "git_fit/privacy/polyline_filter"
56
57
  require_relative "git_fit/sync/base"
57
58
  require_relative "git_fit/sync/strava"
58
59
  require_relative "git_fit/db/connection"
59
60
  require_relative "git_fit/db/activity"
61
+ require_relative "git_fit/export"
60
62
  require_relative "git_fit/db/cli"
63
+ require_relative "git_fit/cli/export"
61
64
  require_relative "git_fit/cli"
@@ -0,0 +1,25 @@
1
+ require "thor"
2
+
3
+ module GitFit
4
+ class ExportCLI < Thor
5
+ desc "json", "Export activities to JSON"
6
+ option :output, type: :string, aliases: "-o", desc: "Output path"
7
+ option :activity, type: :array, desc: "Filter by sport type", aliases: "--act"
8
+ no_commands do
9
+ def git_fit_config
10
+ @git_fit_config ||= GitFit::Config.new(options[:config])
11
+ end
12
+ end
13
+
14
+ def json
15
+ config = git_fit_config
16
+ db = GitFit::DB::Connection.new(config.db_path).db
17
+ path = options[:output] || config.export_config.dig("json", "path") || "site/activities.json"
18
+ filter = options[:activity]&.map(&:strip)&.map(&:downcase)
19
+ count = Export::JSON.new(db: db, output: path, activity_filter: filter).call
20
+ say_status :done, "Exported #{count} activities to #{path}", :green
21
+ rescue => e
22
+ say_status :error, "JSON export failed: #{e.message}", :red
23
+ end
24
+ end
25
+ end
data/lib/git_fit/cli.rb CHANGED
@@ -13,6 +13,9 @@ module GitFit
13
13
  desc "db SUBCOMMAND", "Database operations"
14
14
  subcommand "db", DB::CLI
15
15
 
16
+ desc "export SUBCOMMAND", "Export activities to various formats"
17
+ subcommand "export", ExportCLI
18
+
16
19
  desc "version", "Show version"
17
20
  def version
18
21
  puts "git-fit v#{GitFit::VERSION}"
@@ -0,0 +1,29 @@
1
+ module GitFit
2
+ module Export
3
+ module Defaults
4
+ SVG = {
5
+ background: "#222222", text: "#FFFFFF", track: "#4DD2FF",
6
+ track2: "#4DD2FF", special: "#FFFF00", special2: "#FFFF00",
7
+ empty_cell: "#444444", dim_past: "#555555", font: "Arial",
8
+ }.freeze
9
+
10
+ DASHBOARD = {
11
+ background: "#1a1a2e", surface: "#16213e", hover: "#0f3460",
12
+ active_row: "#1a3a6a", border: "#2a2a4a", text: "#e0e0e0",
13
+ text_secondary: "#888", text_dim: "#666", text_insight: "#aaa",
14
+ accent: "#4DD2FF", font: "-apple-system, BlinkMacSystemFont, sans-serif",
15
+ }.freeze
16
+
17
+ CATEGORIES = {
18
+ "run" => "#4DD2FF", "walk" => "#4ECDC4",
19
+ "ride" => "#FF6B6B", "hike" => "#FFD93D",
20
+ }.freeze
21
+
22
+ MAP = {
23
+ tiles_dark: "https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png",
24
+ tiles_light: "https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png",
25
+ echarts_theme: "dark",
26
+ }.freeze
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,112 @@
1
+ require "json"
2
+
3
+ module GitFit
4
+ module Export
5
+ class JSON
6
+ PLACEHOLDER_POLYLINE = "gqqrFurkeU??".freeze
7
+
8
+ def initialize(db:, output:, activity_filter: nil)
9
+ @db = db
10
+ @output = output
11
+ @activity_filter = activity_filter
12
+ end
13
+
14
+ def call
15
+ dataset = @db[:activities].order(Sequel.desc(:start_date))
16
+ dataset = dataset.where(sport_category: @activity_filter) if @activity_filter
17
+
18
+ activities = dataset.all
19
+
20
+ run_id_to_group = {}
21
+ @db[:dedup_group_members].each { |gm| run_id_to_group[gm[:run_id]] = gm[:group_id] }
22
+
23
+ group_members_cache = {}
24
+
25
+ formatted = activities.map do |a|
26
+ gid = run_id_to_group[a[:run_id]]
27
+ if gid
28
+ group_members_cache[gid] ||= @db[:dedup_group_members].where(group_id: gid).all
29
+ composite_activity(a, gid, group_members_cache[gid])
30
+ else
31
+ format_activity(a)
32
+ end
33
+ end
34
+
35
+ File.write(@output, ::JSON.pretty_generate(formatted))
36
+ formatted.size
37
+ end
38
+
39
+ private
40
+
41
+ def format_activity(a)
42
+ base = {
43
+ run_id: a[:run_id],
44
+ name: a[:name],
45
+ distance: a[:distance],
46
+ moving_time: a[:moving_time],
47
+ type: a[:sport_category],
48
+ sport_type: a[:sport_type],
49
+ start_date: a[:start_date],
50
+ start_date_local: a[:start_date_local],
51
+ location_country: a[:location_country],
52
+ summary_polyline: a[:source] == "keep" && a[:summary_polyline] == PLACEHOLDER_POLYLINE ?
53
+ nil : a[:summary_polyline],
54
+ average_heartrate: a[:average_heartrate] && a[:average_heartrate] != 0 ? a[:average_heartrate] : nil,
55
+ max_heartrate: a[:max_heartrate],
56
+ average_cadence: a[:average_cadence],
57
+ max_cadence: a[:max_cadence],
58
+ average_power: a[:average_power],
59
+ max_power: a[:max_power],
60
+ calories: a[:calories],
61
+ average_temperature: a[:average_temperature],
62
+ average_speed: a[:average_speed],
63
+ elevation_gain: a[:elevation_gain],
64
+ source: a[:source],
65
+ }
66
+ if a[:divisions]
67
+ base[:divisions] = ::JSON.parse(a[:divisions])
68
+ end
69
+ base
70
+ end
71
+
72
+ def composite_activity(activity, group_id, members)
73
+ run_ids = members.map { |m| m[:run_id] }
74
+ group_activities = @db[:activities].where(run_id: run_ids).all
75
+
76
+ composite = if defined?(GitFit::Dedup::FieldElector)
77
+ elector = GitFit::Dedup::FieldElector.new
78
+ result = {}
79
+ GitFit::Dedup::FieldElector::FIELD_ELECTION_RULES.each_key do |field|
80
+ values = group_activities.map { |a| a[field] }
81
+ sources = group_activities.map { |a| a[:source]&.to_sym }
82
+ result[field] = elector.elect(field, values, sources)
83
+ end
84
+ result
85
+ else
86
+ members.first&.slice(:run_id, :distance, :moving_time, :elapsed_time,
87
+ :average_heartrate, :max_heartrate, :elevation_gain,
88
+ :average_speed, :average_cadence, :max_cadence) || {}
89
+ end
90
+
91
+ composite[:run_id] = activity[:run_id]
92
+ composite[:source] = activity[:source]
93
+ composite[:sport_type] = activity[:sport_type]
94
+ composite[:start_date_local] = activity[:start_date_local]
95
+ composite[:location_country] = activity[:location_country]
96
+ composite[:sport_category] = activity[:sport_category]
97
+
98
+ if composite[:distance].to_f > 0 && composite[:moving_time].to_f > 0
99
+ composite[:average_speed] = composite[:distance] / composite[:moving_time]
100
+ end
101
+
102
+ composite[:type] = composite[:sport_category]
103
+ composite[:dedup_group] = group_id
104
+ composite[:member_sources] = members.map do |m|
105
+ { source: m[:source], run_id: m[:run_id], flag: m[:flag], confidence: m[:confidence] }
106
+ end
107
+
108
+ composite
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,7 @@
1
+ module GitFit
2
+ module Export
3
+ end
4
+ end
5
+
6
+ require_relative "export/defaults"
7
+ require_relative "export/json"
@@ -0,0 +1,155 @@
1
+ require "date"
2
+
3
+ module GitFit
4
+ module Source
5
+ class Base
6
+ def initialize(config:, db:, activity_filter: nil, privacy: nil, time_budget: nil)
7
+ @config = config
8
+ @db = db
9
+ @activity_filter = activity_filter
10
+ @privacy = privacy
11
+ @time_budget = time_budget
12
+ @start_time = nil
13
+ @progress_reported = false
14
+ end
15
+
16
+ attr_reader :progress_reported
17
+
18
+ def call
19
+ return 0 unless before_call
20
+ return 0 unless respond_to?(:authenticate) ? authenticate : true
21
+ ids = pending_ids
22
+ return 0 if ids.nil? || ids.empty?
23
+ total = ids.size
24
+ count = 0
25
+ ids.filter_map do |p|
26
+ r = process_one(p)
27
+ count += 1
28
+ report_progress(count, total, build_progress_info(r)) if r
29
+ r
30
+ end.size
31
+ end
32
+
33
+ def before_call
34
+ @start_time = Time.now
35
+ true
36
+ end
37
+
38
+ def pending_ids
39
+ raise NotImplementedError
40
+ end
41
+
42
+ def process_one(pending)
43
+ raise NotImplementedError
44
+ end
45
+
46
+ def build_progress_info(attrs)
47
+ parts = []
48
+ sd = attrs["start_date"] || attrs[:start_date]
49
+ if sd
50
+ d = Date.parse(sd.to_s) rescue nil
51
+ if d
52
+ date_str = d.year == Date.today.year ? d.strftime("%m-%d") : d.strftime("%Y-%m-%d")
53
+ parts << date_str
54
+ end
55
+ end
56
+ type = attrs["sport_category"] || attrs[:sport_category]
57
+ parts << type if type
58
+ dist = attrs["distance"] || attrs[:distance]
59
+ if dist && dist.to_f > 0
60
+ parts << "#{format('%.1f', dist.to_f / 1000)}km"
61
+ end
62
+ source = attrs["source"] || attrs[:source] || source_label
63
+ name = attrs["name"] || attrs[:name]
64
+ display_name = name.to_s.strip
65
+ display_name = "#{source} Activity" if display_name.empty?
66
+ chars = display_name.chars
67
+ display_name = chars.first(30).join + "…" if chars.size > 30
68
+ parts << display_name
69
+ parts.join(" | ") unless parts.empty?
70
+ end
71
+
72
+ protected
73
+
74
+ def time_expired?
75
+ return false unless @time_budget.is_a?(Numeric) && @time_budget > 0 && @start_time
76
+ (Time.now - @start_time) > @time_budget
77
+ end
78
+
79
+ def raw_dir
80
+ src = self.class.name.split("::").last.downcase
81
+ File.join("data", "raw", src)
82
+ end
83
+
84
+ def raw_path(platform_id)
85
+ raise NotImplementedError
86
+ end
87
+
88
+ def raw_file_exists?(platform_id)
89
+ File.exist?(raw_path(platform_id))
90
+ end
91
+
92
+ def std_dir
93
+ src = self.class.name.split("::").last.downcase
94
+ File.join("data", "std", src)
95
+ end
96
+
97
+ def std_path(platform_id, ext = "json")
98
+ File.join(std_dir, "#{platform_id}.#{ext}")
99
+ end
100
+
101
+ def report_progress(count, total = nil, info = nil)
102
+ return unless show_progress?(count, total)
103
+ @progress_reported = true
104
+ line = "#{source_label}: #{count}"
105
+ line += "/#{total}" if total
106
+ line += " #{info}" if info
107
+ line += " "
108
+ print "\r#{line}"
109
+ $stdout.flush
110
+ end
111
+
112
+ def show_progress?(count, total)
113
+ return true if count == 1
114
+ return true if total && (total - count) <= 50
115
+ step = progress_step(total)
116
+ step <= 1 || count % step == 0
117
+ end
118
+
119
+ def progress_step(total)
120
+ return 1 unless total && total > 0
121
+ [1, (1.5 * (total ** 0.4)).to_i].max
122
+ end
123
+
124
+ def source_label
125
+ name = self.class.name
126
+ name ? name.split("::").last : "Source"
127
+ end
128
+
129
+ def upsert_activity(attrs)
130
+ attrs = attrs.transform_keys(&:to_s)
131
+ now = Time.now.utc
132
+
133
+ if @privacy && !@privacy.before_saving?
134
+ attrs["summary_polyline"] = @privacy.filter(attrs["summary_polyline"])
135
+ end
136
+
137
+ existing = @db[:activities].where(run_id: attrs["run_id"]).first
138
+ if existing
139
+ attrs.delete("created_at")
140
+ attrs["updated_at"] = now
141
+ @db[:activities].where(run_id: attrs["run_id"]).update(attrs)
142
+ else
143
+ attrs["created_at"] ||= now
144
+ attrs["updated_at"] ||= now
145
+ @db[:activities].insert(attrs)
146
+ end
147
+ end
148
+
149
+ def skip_type?(sport_type)
150
+ return false unless @activity_filter
151
+ !@activity_filter.include?(sport_type.to_s.downcase)
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,55 @@
1
+ module GitFit
2
+ module Source
3
+ class Tally
4
+ DIGITS = %w[零 一 二 三 四 五 六 七 八 九 十]
5
+
6
+ def initialize(count = 0)
7
+ @count = count
8
+ end
9
+
10
+ def add(n = 1)
11
+ @count += n
12
+ end
13
+
14
+ def to_s
15
+ return @count.to_s if @count > 9999
16
+ chinese_numeral
17
+ rescue StandardError
18
+ @count.to_s
19
+ end
20
+
21
+ private
22
+
23
+ def chinese_numeral
24
+ n = @count
25
+ return DIGITS[0] if n == 0
26
+ return DIGITS[n] if n <= 10
27
+
28
+ result = ""
29
+ thousands = n / 1000
30
+ n %= 1000
31
+ result += DIGITS[thousands] + "千" if thousands > 0
32
+
33
+ hundreds = n / 100
34
+ n %= 100
35
+ if hundreds > 0
36
+ result += DIGITS[hundreds] + "百"
37
+ elsif thousands > 0 && n > 0
38
+ result += "零"
39
+ end
40
+
41
+ tens = n / 10
42
+ ones = n % 10
43
+ if tens > 0
44
+ join_ten = tens > 1 || !result.empty?
45
+ result += (join_ten ? DIGITS[tens] : "") + "十"
46
+ elsif !result.empty? && tens == 0 && ones > 0 && !result.end_with?("零")
47
+ result += "零"
48
+ end
49
+
50
+ result += DIGITS[ones] if ones > 0
51
+ result
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,7 @@
1
+ module GitFit
2
+ module Source
3
+ end
4
+ end
5
+
6
+ require_relative "source/tally"
7
+ require_relative "source/base"
@@ -1,3 +1,3 @@
1
1
  module GitFit
2
- VERSION = "0.3.2"
2
+ VERSION = "0.4.1"
3
3
  end
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.3.2
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lax
@@ -105,11 +105,15 @@ files:
105
105
  - exe/git-fit
106
106
  - lib/git-fit.rb
107
107
  - lib/git_fit/cli.rb
108
+ - lib/git_fit/cli/export.rb
108
109
  - lib/git_fit/config.rb
109
110
  - lib/git_fit/config_template.rb
110
111
  - lib/git_fit/db/activity.rb
111
112
  - lib/git_fit/db/cli.rb
112
113
  - lib/git_fit/db/connection.rb
114
+ - lib/git_fit/export.rb
115
+ - lib/git_fit/export/defaults.rb
116
+ - lib/git_fit/export/json.rb
113
117
  - lib/git_fit/fit.rb
114
118
  - lib/git_fit/fit/decoder.rb
115
119
  - lib/git_fit/geo.rb
@@ -121,6 +125,9 @@ files:
121
125
  - lib/git_fit/parser/gpx.rb
122
126
  - lib/git_fit/parser/tcx.rb
123
127
  - lib/git_fit/privacy/polyline_filter.rb
128
+ - lib/git_fit/source.rb
129
+ - lib/git_fit/source/base.rb
130
+ - lib/git_fit/source/tally.rb
124
131
  - lib/git_fit/sport_mapper.rb
125
132
  - lib/git_fit/sync/base.rb
126
133
  - lib/git_fit/sync/strava.rb