-
Notifications
You must be signed in to change notification settings - Fork 6
/
app.rb
193 lines (154 loc) · 5.61 KB
/
app.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
require 'bundler/setup'
require 'sinatra/base'
require 'sinatra/flash'
require 'sinatra/reloader'
require 'health_graph'
require 'nike'
require 'geoutm'
APP_DOMAIN = ENV['APP_DOMAIN'] || 'http://localhost:9292'
APP_SECRET = ENV['APP_SECRET'] || 'nikeplus-to-runkeeper'
RUNKEEPER_CLIENT_ID = ENV['RUNKEEPER_CLIENT_ID']
RUNKEEPER_CLIENT_SECRET = ENV['RUNKEEPER_CLIENT_SECRET']
HealthGraph.configure do |config|
config.client_id = RUNKEEPER_CLIENT_ID
config.client_secret = RUNKEEPER_CLIENT_SECRET
config.authorization_redirect_url = "#{APP_DOMAIN}/auth/runkeeper/callback"
end
RunkeeperUser = Struct.new(:id, :token, :username, :fullname) do
def display_name
fullname || username
end
end
class NikePlusToRunkeeperImporter < Sinatra::Base
use Rack::Session::Cookie, key: 'nikeplus-to-runkeeper', secret: APP_SECRET
register Sinatra::Flash
configure :development do
register Sinatra::Reloader
end
helpers do
def user
if !@user && signed_in?
hash = session[:user]
@user = RunkeeperUser.new(hash[:id], hash[:token], hash[:username], hash[:fullname])
end
@user
end
def signed_in?
session.has_key?(:user)
end
end
get '/' do
if signed_in?
redirect to('/import')
else
%(<a href="#{url('/auth/runkeeper')}">Login</a>)
end
end
get '/import' do
return redirect to('/') unless signed_in?
day = 60 * 60 * 24
periods = [
{ name: 'Last week', value: Time.now - (day * 7) },
{ name: 'Last 30 days', value: Time.now - (day * 30) },
{ name: 'Last year', value: Time.now - (day * 365) },
{ name: 'Everything', value: '' }
]
erb(:import, locals: { periods: periods })
end
post '/import' do
return redirect to('/') unless signed_in?
nike_client = Nike::Client.new(params[:email], params[:password])
begin
nike_activities = nike_client.activities
rescue RuntimeError => e
raise e unless e.message == "add_cookies only takes a Hash or a String"
flash[:info] = "Failed to log in to NikePlus, are your email and password correct?"
return redirect to('/import')
end
activity_cutoff = Time.parse(params[:activity_since]) unless params[:activity_since].to_s.empty?
runkeeper_activities = nike_activities.map do |a|
next if activity_cutoff && a.start_time_utc < activity_cutoff
nike_activity = nike_client.activity(a.activity_id)
duration = nike_activity.duration / 1000.0
runkeeper_activity = {
type: 'Running',
start_time: nike_activity.start_time_utc + nike_activity.start_time_utc.gmt_offset, # hack: adjust time by gmt_offset to get in local time, which is what Runkeeper wants
total_distance: nike_activity.distance * 1000,
duration: duration,
detect_pauses: true,
total_calories: nike_activity.calories.to_f,
average_heart_rate: nike_activity.average_heart_rate.to_f,
}
if nike_activity.tags.note
runkeeper_activity.notes = nike_activity.tags.note
end
if a.gps && nike_activity.geo && nike_activity.geo.waypoints
index = -1
total = nike_activity.geo.waypoints.size
fraction = duration / total
last_path = nil
last_delta = nil
paths = []
nike_activity.geo.waypoints.each_with_index do |wp, index|
type = 'gps'
type = 'start' if index == 0
type = 'end' if (index + 1) == total
path = {
timestamp: fraction * (index += 1),
altitude: wp['ele'],
longitude: wp['lon'],
latitude: wp['lat'],
type: type
}
# Account for pauses in the run by calculating the distance between
# waypoints, when we detect a large enough jump we assume the run
# was paused and add a "pause" waypoing into the path.
if last_path
next_utm = GeoUtm::LatLon.new(path[:latitude], path[:longitude]).to_utm
last_utm = GeoUtm::LatLon.new(last_path[:latitude], last_path[:longitude]).to_utm
delta = last_utm.distance_to(next_utm)
if last_delta && delta > 0 && delta > (last_delta * 2)
# For some reason the API does not like a "resume" node to be
# added afterwards but will detect the next point just fine.
paused_path = last_path.clone
paused_path[:type] = 'pause'
paths << paused_path.clone
end
last_delta = delta if delta > 0
end
paths << last_path = path
end
runkeeper_activity[:path] = paths
end
runkeeper_activity
end.compact
erb(:export, locals: { activities: runkeeper_activities })
end
post '/export' do
return redirect to('/') unless signed_in?
activities = params[:activities]
activities.each do |activity|
parsed = JSON.parse(activity, symbolize_names: true)
parsed[:start_time] = Time.parse(parsed[:start_time]).httpdate
HealthGraph::NewFitnessActivity.new(user.token, parsed)
end
flash[:info] = "Successfully imported #{activities.size} activities"
redirect to('/import')
end
get '/auth/runkeeper' do
redirect to(HealthGraph.authorize_url)
end
get '/auth/runkeeper/callback' do
raise 'Authentication Error' unless params[:code]
access_token = HealthGraph.access_token(params[:code])
user = HealthGraph::User.new(access_token)
profile = user.profile
session[:user] = {
id: user.userID,
username: profile.profile.split('/').last,
fullname: profile.name,
token: access_token
}
redirect to('/import')
end
end