fatigue 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +146 -0
- data/bin/fatigue +55 -0
- data/fatigue.gemspec +85 -0
- data/lib/fatigue.rb +12 -0
- data/lib/fatigue/garmin.rb +110 -0
- data/lib/fatigue/nike.rb +72 -0
- data/lib/fatigue/run.rb +108 -0
- data/readme.markdown +66 -0
- data/test/fixtures/runs.xml +41 -0
- data/test/fixtures/user.xml +59 -0
- data/test/helper.rb +42 -0
- data/test/test_garmin.rb +19 -0
- data/test/test_nike.rb +19 -0
- data/test/test_run.rb +43 -0
- metadata +147 -0
data/Rakefile
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
#############################################################################
|
6
|
+
#
|
7
|
+
# Helper functions
|
8
|
+
#
|
9
|
+
#############################################################################
|
10
|
+
|
11
|
+
def name
|
12
|
+
@name ||= Dir['*.gemspec'].first.split('.').first
|
13
|
+
end
|
14
|
+
|
15
|
+
def version
|
16
|
+
line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
|
17
|
+
line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
|
18
|
+
end
|
19
|
+
|
20
|
+
def date
|
21
|
+
Date.today.to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
def rubyforge_project
|
25
|
+
name
|
26
|
+
end
|
27
|
+
|
28
|
+
def gemspec_file
|
29
|
+
"#{name}.gemspec"
|
30
|
+
end
|
31
|
+
|
32
|
+
def gem_file
|
33
|
+
"#{name}-#{version}.gem"
|
34
|
+
end
|
35
|
+
|
36
|
+
def replace_header(head, header_name)
|
37
|
+
head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
|
38
|
+
end
|
39
|
+
|
40
|
+
#############################################################################
|
41
|
+
#
|
42
|
+
# Standard tasks
|
43
|
+
#
|
44
|
+
#############################################################################
|
45
|
+
|
46
|
+
task :default => :test
|
47
|
+
|
48
|
+
require 'rake/testtask'
|
49
|
+
Rake::TestTask.new(:test) do |test|
|
50
|
+
test.libs << 'lib' << 'test'
|
51
|
+
test.pattern = 'test/**/test_*.rb'
|
52
|
+
test.verbose = true
|
53
|
+
end
|
54
|
+
|
55
|
+
desc "Generate RCov test coverage and open in your browser"
|
56
|
+
task :coverage do
|
57
|
+
require 'rcov'
|
58
|
+
sh "rm -fr coverage"
|
59
|
+
sh "rcov test/test_*.rb"
|
60
|
+
sh "open coverage/index.html"
|
61
|
+
end
|
62
|
+
|
63
|
+
require 'rake/rdoctask'
|
64
|
+
Rake::RDocTask.new do |rdoc|
|
65
|
+
rdoc.rdoc_dir = 'rdoc'
|
66
|
+
rdoc.title = "#{name} #{version}"
|
67
|
+
rdoc.rdoc_files.include('README*')
|
68
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
69
|
+
end
|
70
|
+
|
71
|
+
desc "Open an irb session preloaded with this library"
|
72
|
+
task :console do
|
73
|
+
sh "irb -rubygems -r ./lib/#{name}.rb"
|
74
|
+
end
|
75
|
+
|
76
|
+
#############################################################################
|
77
|
+
#
|
78
|
+
# Custom tasks (add your own tasks here)
|
79
|
+
#
|
80
|
+
#############################################################################
|
81
|
+
|
82
|
+
|
83
|
+
|
84
|
+
#############################################################################
|
85
|
+
#
|
86
|
+
# Packaging tasks
|
87
|
+
#
|
88
|
+
#############################################################################
|
89
|
+
|
90
|
+
task :release => :build do
|
91
|
+
unless `git branch` =~ /^\* master$/
|
92
|
+
puts "You must be on the master branch to release!"
|
93
|
+
exit!
|
94
|
+
end
|
95
|
+
sh "git commit --allow-empty -a -m 'Release #{version}'"
|
96
|
+
sh "git tag v#{version}"
|
97
|
+
sh "git push origin master"
|
98
|
+
sh "git push origin v#{version}"
|
99
|
+
sh "gem push pkg/#{name}-#{version}.gem"
|
100
|
+
end
|
101
|
+
|
102
|
+
task :build => :gemspec do
|
103
|
+
sh "mkdir -p pkg"
|
104
|
+
sh "gem build #{gemspec_file}"
|
105
|
+
sh "mv #{gem_file} pkg"
|
106
|
+
end
|
107
|
+
|
108
|
+
task :gemspec => :validate do
|
109
|
+
# read spec file and split out manifest section
|
110
|
+
spec = File.read(gemspec_file)
|
111
|
+
head, manifest, tail = spec.split(" # = MANIFEST =\n")
|
112
|
+
|
113
|
+
# replace name version and date
|
114
|
+
replace_header(head, :name)
|
115
|
+
replace_header(head, :version)
|
116
|
+
replace_header(head, :date)
|
117
|
+
#comment this out if your rubyforge_project has a different name
|
118
|
+
replace_header(head, :rubyforge_project)
|
119
|
+
|
120
|
+
# determine file list from git ls-files
|
121
|
+
files = `git ls-files`.
|
122
|
+
split("\n").
|
123
|
+
sort.
|
124
|
+
reject { |file| file =~ /^\./ }.
|
125
|
+
reject { |file| file =~ /^(rdoc|pkg)/ }.
|
126
|
+
map { |file| " #{file}" }.
|
127
|
+
join("\n")
|
128
|
+
|
129
|
+
# piece file back together and write
|
130
|
+
manifest = " s.files = %w[\n#{files}\n ]\n"
|
131
|
+
spec = [head, manifest, tail].join(" # = MANIFEST =\n")
|
132
|
+
File.open(gemspec_file, 'w') { |io| io.write(spec) }
|
133
|
+
puts "Updated #{gemspec_file}"
|
134
|
+
end
|
135
|
+
|
136
|
+
task :validate do
|
137
|
+
libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
|
138
|
+
unless libfiles.empty?
|
139
|
+
puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir."
|
140
|
+
exit!
|
141
|
+
end
|
142
|
+
unless Dir['VERSION*'].empty?
|
143
|
+
puts "A `VERSION` file at root level violates Gem best practices."
|
144
|
+
exit!
|
145
|
+
end
|
146
|
+
end
|
data/bin/fatigue
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'fatigue'
|
7
|
+
|
8
|
+
art_lol = <<ART
|
9
|
+
|
10
|
+
__ _ _
|
11
|
+
/ _| | | (_)
|
12
|
+
| |_ __ _| |_ _ __ _ _ _ ___
|
13
|
+
| _/ _` | __| |/ _` | | | |/ _ \\
|
14
|
+
| || (_| | |_| | (_| | |_| | __/
|
15
|
+
|_| \\__,_|\\__|_|\\__, |\\__,_|\\___|
|
16
|
+
__/ | for nike+garmin
|
17
|
+
|___/
|
18
|
+
|
19
|
+
ART
|
20
|
+
|
21
|
+
puts "\n"
|
22
|
+
puts art_lol
|
23
|
+
puts "\n"
|
24
|
+
|
25
|
+
print 'Nike+ User ID number: '
|
26
|
+
id = gets.chomp
|
27
|
+
|
28
|
+
print 'Garmin Connect Username: '
|
29
|
+
garmin_username = gets.chomp
|
30
|
+
|
31
|
+
print 'Garmin Connect Password: '
|
32
|
+
system "stty -echo"
|
33
|
+
garmin_password = gets.chomp
|
34
|
+
system "stty echo"
|
35
|
+
|
36
|
+
nike = Fatigue::Nike.new(id)
|
37
|
+
garmin = Fatigue::Garmin.new(garmin_username,garmin_password)
|
38
|
+
|
39
|
+
puts "\n\nHARDCORE RUNNING ACTION: "
|
40
|
+
|
41
|
+
if garmin.login
|
42
|
+
number = garmin.post_runs(nike.runs)
|
43
|
+
|
44
|
+
puts <<DONE
|
45
|
+
|
46
|
+
|
47
|
+
|
48
|
+
Neato! We just imported #{number} run#{'s' if number!=1}. See them for yourself at:
|
49
|
+
http://connect.garmin.com/activities
|
50
|
+
|
51
|
+
DONE
|
52
|
+
|
53
|
+
else
|
54
|
+
puts "\nThere was a problem logging you into Garmin Connect."
|
55
|
+
end
|
data/fatigue.gemspec
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
## This is the rakegem gemspec template. Make sure you read and understand
|
2
|
+
## all of the comments. Some sections require modification, and others can
|
3
|
+
## be deleted if you don't need them. Once you understand the contents of
|
4
|
+
## this file, feel free to delete any comments that begin with two hash marks.
|
5
|
+
## You can find comprehensive Gem::Specification documentation, at
|
6
|
+
## http://docs.rubygems.org/read/chapter/20
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.specification_version = 2 if s.respond_to? :specification_version=
|
9
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
10
|
+
s.rubygems_version = '1.3.5'
|
11
|
+
|
12
|
+
## Leave these as is they will be modified for you by the rake gemspec task.
|
13
|
+
## If your rubyforge_project name is different, then edit it and comment out
|
14
|
+
## the sub! line in the Rakefile
|
15
|
+
s.name = 'fatigue'
|
16
|
+
s.version = '0.0.1'
|
17
|
+
s.date = '2010-09-12'
|
18
|
+
s.rubyforge_project = 'fatigue'
|
19
|
+
|
20
|
+
## Make sure your summary is short. The description may be as long
|
21
|
+
## as you like.
|
22
|
+
s.summary = "Import your Nike+ runs into Garmin Connect."
|
23
|
+
s.description = "A tiny little gem that helps you load your Nike+ run history
|
24
|
+
into Garmin Connect. Cool, huh?."
|
25
|
+
|
26
|
+
## List the primary authors. If there are a bunch of authors, it's probably
|
27
|
+
## better to set the email to an email list or something. If you don't have
|
28
|
+
## a custom homepage, consider using your GitHub URL or the like.
|
29
|
+
s.authors = ["Zach Holman"]
|
30
|
+
s.email = 'fatigue@zachholman.com'
|
31
|
+
s.homepage = 'http://github.com/holman/fatigue'
|
32
|
+
|
33
|
+
## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as
|
34
|
+
## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb'
|
35
|
+
s.require_paths = %w[lib]
|
36
|
+
|
37
|
+
## This sections is only necessary if you have C extensions.
|
38
|
+
# s.require_paths << 'ext'
|
39
|
+
# s.extensions = %w[ext/extconf.rb]
|
40
|
+
|
41
|
+
## If your gem includes any executables, list them here.
|
42
|
+
s.executables = ["fatigue"]
|
43
|
+
s.default_executable = 'fatigue'
|
44
|
+
|
45
|
+
## Specify any RDoc options here. You'll want to add your README and
|
46
|
+
## LICENSE files to the extra_rdoc_files list.
|
47
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
48
|
+
s.extra_rdoc_files = %w[readme.markdown]
|
49
|
+
|
50
|
+
## List your runtime dependencies here. Runtime dependencies are those
|
51
|
+
## that are needed for an end user to actually USE your code.
|
52
|
+
s.add_dependency('nokogiri', ">= 1.4.0")
|
53
|
+
s.add_dependency('mechanize', '1.0.0')
|
54
|
+
s.add_dependency('ruby-progressbar', '0.0.9')
|
55
|
+
|
56
|
+
## List your development dependencies here. Development dependencies are
|
57
|
+
## those that are only needed during development
|
58
|
+
s.add_development_dependency('rr', '0.10.11')
|
59
|
+
|
60
|
+
## Leave this section as-is. It will be automatically generated from the
|
61
|
+
## contents of your Git repository via the gemspec task. DO NOT REMOVE
|
62
|
+
## THE MANIFEST COMMENTS, they are used as delimiters by the task.
|
63
|
+
# = MANIFEST =
|
64
|
+
s.files = %w[
|
65
|
+
Rakefile
|
66
|
+
bin/fatigue
|
67
|
+
fatigue.gemspec
|
68
|
+
lib/fatigue.rb
|
69
|
+
lib/fatigue/garmin.rb
|
70
|
+
lib/fatigue/nike.rb
|
71
|
+
lib/fatigue/run.rb
|
72
|
+
readme.markdown
|
73
|
+
test/fixtures/runs.xml
|
74
|
+
test/fixtures/user.xml
|
75
|
+
test/helper.rb
|
76
|
+
test/test_garmin.rb
|
77
|
+
test/test_nike.rb
|
78
|
+
test/test_run.rb
|
79
|
+
]
|
80
|
+
# = MANIFEST =
|
81
|
+
|
82
|
+
## Test files will be grabbed from the file list. Make sure the path glob
|
83
|
+
## matches what you actually use.
|
84
|
+
s.test_files = s.files.select { |path| path =~ /^test\/test_.*\.rb/ }
|
85
|
+
end
|
data/lib/fatigue.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
module Fatigue
|
2
|
+
|
3
|
+
# GARMIN
|
4
|
+
# A class used to access Garmin Connect.
|
5
|
+
class Garmin
|
6
|
+
|
7
|
+
# Public: Create a new Garmin instance.
|
8
|
+
#
|
9
|
+
# username - The String of the Garmin Connect username.
|
10
|
+
# password - The String of the Garmin Connect password.
|
11
|
+
#
|
12
|
+
# Returns a new Fatigue::Garmin instance.
|
13
|
+
def initialize(username, password)
|
14
|
+
@username = username
|
15
|
+
@password = password
|
16
|
+
end
|
17
|
+
|
18
|
+
# Public: logs the instance into Garmin Connect.
|
19
|
+
#
|
20
|
+
# Returns true if successful login, false if unsuccessful.
|
21
|
+
def login
|
22
|
+
@agent = Mechanize.new
|
23
|
+
login_page = @agent.get('https://connect.garmin.com/signin')
|
24
|
+
form = login_page.form('login')
|
25
|
+
form['login:loginUsernameField'] = @username
|
26
|
+
form['login:password'] = @password
|
27
|
+
page = @agent.submit(form, form.buttons.first)
|
28
|
+
page.inspect =~ /Dashboard for/ ? true : false
|
29
|
+
end
|
30
|
+
|
31
|
+
# Public: logs the instance out of Garmin Connect.
|
32
|
+
#
|
33
|
+
# This is pretty important since Garmin hates multi-logins. Apparently?
|
34
|
+
#
|
35
|
+
# Returns true if successful logout, false if unsuccessful.
|
36
|
+
def logout
|
37
|
+
page = @agent.get('http://connect.garmin.com/dashboard')
|
38
|
+
page.link_with(:text => 'Sign Out').click
|
39
|
+
page.inspect =~ /aren't signed in/ ? true : false
|
40
|
+
end
|
41
|
+
|
42
|
+
# Public: posts a new Run to your logged-in Garmin account.
|
43
|
+
#
|
44
|
+
# run - A Fatigue::Run instance.
|
45
|
+
#
|
46
|
+
# Returns true if success, false if failure. Requires #login to be called
|
47
|
+
# prior to execution.
|
48
|
+
def post_run(run)
|
49
|
+
manual_run = @agent.get('http://connect.garmin.com/activity/manual')
|
50
|
+
form = manual_run.form('manualActivityForm')
|
51
|
+
form.activityBeginDate = run.started_at.strftime('%m/%d/%Y')
|
52
|
+
form.activityBeginTime = run.started_at.strftime('%I:%M %p')
|
53
|
+
form.field_with(:name => 'activityTimeZoneDecoration:activityTimeZone').
|
54
|
+
options.select { |option|
|
55
|
+
# select option where our timezones are equal (-07:00, etc)
|
56
|
+
option.text.gsub(':','') =~ /#{run.started_at.strftime('%z')}/
|
57
|
+
}.first.select
|
58
|
+
form['activityNameDecoration:activityName'] = run.formatted_name
|
59
|
+
form.field_with(:name => 'activityTypeDecoration:activityType').
|
60
|
+
options.select { |option| option.value == 'running' }.
|
61
|
+
first.select
|
62
|
+
form['speedPaceContainer:activitySummarySumDuration'] = run.hours
|
63
|
+
form['speedPaceContainer:activitySummarySumDurationMinute'] = run.minutes
|
64
|
+
form['speedPaceContainer:activitySummarySumDurationSecond'] = run.seconds
|
65
|
+
form['speedPaceContainer:activitySummarySumDistanceDecoration:activitySummarySumDistance'] = run.distance
|
66
|
+
form.field_with(:name => 'speedPaceContainer:activitySummarySumDistanceDecoration:distanceUnit').
|
67
|
+
options.select { |option| option.text == run.unit }.first.select
|
68
|
+
form['descriptionDecoration:description'] = run.description
|
69
|
+
form['calories'] = run.calories
|
70
|
+
form['AJAXREQUEST'] = '_viewRoot'
|
71
|
+
form['saveButton'] = 'saveButton'
|
72
|
+
|
73
|
+
resp = @agent.submit(form, form.buttons_with(:name => 'saveButton').first)
|
74
|
+
|
75
|
+
verify_response(resp.body)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Public: Posts a collection of Runs to your logged-in Garmin account.
|
79
|
+
#
|
80
|
+
# runs - An Array of Fatigue::Run instances.
|
81
|
+
#
|
82
|
+
# Returns the number of runs successfully posted to the Garmin account.
|
83
|
+
# Requires #login to be called prior to execution.
|
84
|
+
def post_runs(runs)
|
85
|
+
progress = ProgressBar.new(" status", runs.size)
|
86
|
+
runs.each do |run|
|
87
|
+
post_run(run)
|
88
|
+
progress.inc
|
89
|
+
end
|
90
|
+
progress.finish
|
91
|
+
runs.size
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
# INTERNAL METHODS #########################################################
|
96
|
+
|
97
|
+
# Verifies whether the response returned is successful or not. For some
|
98
|
+
# reason Garmin isn't spitting out a redirect to the newly-created run, so
|
99
|
+
# instead we hackishly check if an error <span> is filled.
|
100
|
+
#
|
101
|
+
# Returns a Boolean value for whether the response was successful or not.
|
102
|
+
def verify_response(html_body)
|
103
|
+
html = Nokogiri::HTML(html_body)
|
104
|
+
html.css('#ErrorContainer').text.strip == ''
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
data/lib/fatigue/nike.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
module Fatigue
|
2
|
+
|
3
|
+
# NIKE
|
4
|
+
# Nike grabs your runs from Nike
|
5
|
+
class Nike
|
6
|
+
|
7
|
+
# Public: Create a new Nike+ instance.
|
8
|
+
#
|
9
|
+
# id - Numeric Nike+ user ID (see readme.markdown for determining this)
|
10
|
+
#
|
11
|
+
# Returns a fresh Nike instance.
|
12
|
+
def initialize(id)
|
13
|
+
@id = id
|
14
|
+
end
|
15
|
+
|
16
|
+
# Public: Get all the runs for this user
|
17
|
+
#
|
18
|
+
# Returns an array of Fatigue::Run instances.
|
19
|
+
def runs
|
20
|
+
#structure = Nokogiri::XML(user_data_xml)
|
21
|
+
#unit = structure.xpath('.//plusService/userOptions/distanceUnit').
|
22
|
+
# first.text
|
23
|
+
structure = Nokogiri::XML(run_list_xml)
|
24
|
+
structure.xpath('.//plusService/runList/run').inject([]) do |list,xml|
|
25
|
+
run = Fatigue::Run.new
|
26
|
+
run.service = 'Nike+'
|
27
|
+
run.unit = 'km' # I think Nike+ hardcodes km
|
28
|
+
run.name = get_value('name',xml)
|
29
|
+
run.distance = get_value('distance',xml)
|
30
|
+
run.duration = get_value('duration',xml)
|
31
|
+
run.started_at = get_value('startTime',xml)
|
32
|
+
run.calories = get_value('calories',xml)
|
33
|
+
run.description = get_value('description',xml)
|
34
|
+
list << run
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
# INTERNAL METHODS #########################################################
|
40
|
+
|
41
|
+
|
42
|
+
# Builds the Nike+ XML url and retreives it.
|
43
|
+
#
|
44
|
+
# Returns a string of the list of your runs in XML format.
|
45
|
+
def run_list_xml
|
46
|
+
run_url = 'http://nikerunning.nike.com/nikeplus/v1/services/widget/get_public_run_list.jsp?userID='
|
47
|
+
run_url += @id.to_s
|
48
|
+
open(run_url)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Builds the Nike+ user XML url and retreives it. This has unit data.
|
52
|
+
#
|
53
|
+
# Returns a string of your account in XML format.
|
54
|
+
def user_data_xml
|
55
|
+
run_url = 'http://nikerunning.nike.com/nikeplus/v1/services/widget/get_public_user_data.jsp?userID='
|
56
|
+
run_url += @id.to_s
|
57
|
+
open(run_url)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Parses a line of XML for a given attribute.
|
61
|
+
#
|
62
|
+
# attribute - The node we are searching for.
|
63
|
+
# xml - The line of XML we should search in.
|
64
|
+
#
|
65
|
+
# Returns a scalar value of the attribute.
|
66
|
+
def get_value(attribute,xml)
|
67
|
+
xml.xpath(".//#{attribute}").first.content
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
data/lib/fatigue/run.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
module Fatigue
|
2
|
+
|
3
|
+
# RUN
|
4
|
+
# An individual run knows about its distance, time, and other fun.
|
5
|
+
class Run
|
6
|
+
|
7
|
+
# Public: The name of the service used for this run.
|
8
|
+
#
|
9
|
+
# service: The name of the service used for this run.
|
10
|
+
attr_writer :service
|
11
|
+
|
12
|
+
# Public: The name of the service used for this run.
|
13
|
+
attr_reader :service
|
14
|
+
|
15
|
+
|
16
|
+
# Public: The name of the run.
|
17
|
+
#
|
18
|
+
# name: The name of the run used.
|
19
|
+
attr_writer :name
|
20
|
+
|
21
|
+
# Public: The name of the run.
|
22
|
+
attr_reader :name
|
23
|
+
|
24
|
+
# Public: The formatted name of the run. Includes service name.
|
25
|
+
def formatted_name
|
26
|
+
"#{service}#{name.strip == '' ? '' : ': '+name}"
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
# Public: The distance of the run.
|
31
|
+
#
|
32
|
+
# distance: The String unitless distance run.
|
33
|
+
attr_writer :distance
|
34
|
+
|
35
|
+
# Public: The distance of the run.
|
36
|
+
attr_reader :distance
|
37
|
+
|
38
|
+
|
39
|
+
# Public: The distance unit.
|
40
|
+
#
|
41
|
+
# unit: The string value of the unit. Permitted: "mi", "km".
|
42
|
+
attr_writer :unit
|
43
|
+
|
44
|
+
# Public: the full name of the distance unit.
|
45
|
+
def unit
|
46
|
+
case @unit
|
47
|
+
when 'mi' then 'Miles'
|
48
|
+
when 'km' then 'Kilometers'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Public: The duration of the run.
|
53
|
+
#
|
54
|
+
# duration: The String number of milliseconds the run took.
|
55
|
+
attr_writer :duration
|
56
|
+
|
57
|
+
# Public: The Integer duration of the run.
|
58
|
+
def duration
|
59
|
+
@duration.to_i
|
60
|
+
end
|
61
|
+
|
62
|
+
# Public: The number of seconds in the run (reduced).
|
63
|
+
def seconds
|
64
|
+
(duration / 1000) % 60
|
65
|
+
end
|
66
|
+
|
67
|
+
# Public: The number of minutes in the run (reduced).
|
68
|
+
def minutes
|
69
|
+
(duration / 1000 / 60) % 60
|
70
|
+
end
|
71
|
+
|
72
|
+
# Public: The number of hours in the run (reduced).
|
73
|
+
def hours
|
74
|
+
(duration / 1000 / 60 / 60)
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
# Public: The time the run was started.
|
79
|
+
#
|
80
|
+
# started_at: The String time in ISO 8601.
|
81
|
+
attr_writer :started_at
|
82
|
+
|
83
|
+
# Public: The time the run was started.
|
84
|
+
def started_at
|
85
|
+
Time.iso8601 @started_at
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
# Public: The number of calories burned in the run.
|
90
|
+
#
|
91
|
+
# calories: The String number of calories.
|
92
|
+
attr_writer :calories
|
93
|
+
|
94
|
+
# Public: The number of calories burned in the run.
|
95
|
+
attr_reader :calories
|
96
|
+
|
97
|
+
|
98
|
+
# Public: The description of the run.
|
99
|
+
#
|
100
|
+
# description: The String details of the run.
|
101
|
+
attr_writer :description
|
102
|
+
|
103
|
+
# Public: The description of the run.
|
104
|
+
attr_reader :description
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
data/readme.markdown
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# FATIGUE
|
2
|
+
**Import your Nike+ runs to Garmin Connect**
|
3
|
+
|
4
|
+
## It looks like this
|
5
|
+
|
6
|
+
$ fatigue
|
7
|
+
|
8
|
+
|
9
|
+
__ _ _
|
10
|
+
/ _| | | (_)
|
11
|
+
| |_ __ _| |_ _ __ _ _ _ ___
|
12
|
+
| _/ _` | __| |/ _` | | | |/ _ \
|
13
|
+
| || (_| | |_| | (_| | |_| | __/
|
14
|
+
|_| \__,_|\__|_|\__, |\__,_|\___|
|
15
|
+
__/ | for nike+garmin
|
16
|
+
|___/
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
Nike+ User ID: 123456789
|
21
|
+
Garmin Connect Username: sookie
|
22
|
+
Garmin Connect Password:
|
23
|
+
|
24
|
+
HARDCORE RUNNING ACTION:
|
25
|
+
status: 100% |oooooooooooooooooooooooooooooooooooooooooo| Time: 00:02:20
|
26
|
+
|
27
|
+
Neato! We just imported 72 runs. See them for yourself at:
|
28
|
+
http://connect.garmin.com/activities
|
29
|
+
|
30
|
+
## What you get
|
31
|
+
|
32
|
+
This grabs run data from the low-fi Nike+ histories, so while it won't have GPS
|
33
|
+
maps like what Garmin rocks, we'll populate your Garmin history with Nike+ data
|
34
|
+
for things like distance run, how long your run took, what day and time it was
|
35
|
+
run, and nifty things like that.
|
36
|
+
|
37
|
+
## Install
|
38
|
+
|
39
|
+
gem install fatigue
|
40
|
+
|
41
|
+
## Can I see your ID
|
42
|
+
|
43
|
+
Nike might release a public API at some point in the future (although they'll
|
44
|
+
probably make you buy their shoes in order to use it). Until then, we can sneak
|
45
|
+
onto their widget API. You need to find your Nike+ user id, though (numeric ID,
|
46
|
+
that is; not your login).
|
47
|
+
|
48
|
+
To do this, log into your Nike+ account, click on "Runs", and then the long
|
49
|
+
number right at the end of the URL is just what you need. COPY AND PASTE IT
|
50
|
+
WITH ALL DUE HASTE.
|
51
|
+
|
52
|
+
You'll also need to make sure your runs are public. Click on your face on the
|
53
|
+
sidebar, go to settings, and share your life away. No need to be modest.
|
54
|
+
|
55
|
+
## Contribute
|
56
|
+
|
57
|
+
Pretty sure Nike will change their site, Garmin will change their site, or
|
58
|
+
both, so this may be a bit unstable, from time to time. Have no fear; open
|
59
|
+
source is here.
|
60
|
+
|
61
|
+
If you'd like to contribute fixes (thanks!), fork this project, add your
|
62
|
+
changes, make sure tests pass with `rake`, and send me a pull request.
|
63
|
+
High-fives for new methods that are properly [TomDoc](http://tomdoc.org)'d.
|
64
|
+
|
65
|
+
# JUST DO IT™
|
66
|
+
Love, @[holman](http://twitter.com/holman).
|
@@ -0,0 +1,41 @@
|
|
1
|
+
<plusService>
|
2
|
+
<status>success</status>
|
3
|
+
<runList endIndex="-1" startIndex="0">
|
4
|
+
<run id="1" workoutType="standard">
|
5
|
+
<startTime>2009-03-17T04:02:43+00:00</startTime>
|
6
|
+
<distance>3</distance>
|
7
|
+
<duration>900000</duration>
|
8
|
+
<syncTime>2009-03-17T04:02:43+00:00</syncTime>
|
9
|
+
<calories>0.0</calories>
|
10
|
+
<name/>
|
11
|
+
<description/>
|
12
|
+
<howFelt/>
|
13
|
+
<weather/>
|
14
|
+
<terrain/>
|
15
|
+
<intensity/>
|
16
|
+
<gpxId/>
|
17
|
+
<equipmentType>ipod</equipmentType>
|
18
|
+
</run>
|
19
|
+
<run id="2" workoutType="standard">
|
20
|
+
<startTime>2008-05-21T13:28:59-05:00</startTime>
|
21
|
+
<distance>5</distance>
|
22
|
+
<duration>1000000</duration>
|
23
|
+
<syncTime>2008-05-21T18:59:38+00:00</syncTime>
|
24
|
+
<calories>244.0</calories>
|
25
|
+
<name><![CDATA[ran away from the pretty blonde]]></name>
|
26
|
+
<description><![CDATA[ran away from the pretty blonde]]></description>
|
27
|
+
<howFelt/>
|
28
|
+
<weather/>
|
29
|
+
<terrain/>
|
30
|
+
<intensity/>
|
31
|
+
<gpxId/>
|
32
|
+
<equipmentType>ipod</equipmentType>
|
33
|
+
</run>
|
34
|
+
</runList>
|
35
|
+
<runListSummary>
|
36
|
+
<runs>2</runs>
|
37
|
+
<distance>8</distance>
|
38
|
+
<duration>1900000</duration>
|
39
|
+
<calories>244.0</calories>
|
40
|
+
</runListSummary>
|
41
|
+
</plusService>
|
@@ -0,0 +1,59 @@
|
|
1
|
+
<plusService>
|
2
|
+
<status>success</status>
|
3
|
+
<user externalProfileID="1" id="1">
|
4
|
+
<status>confirmed</status>
|
5
|
+
<gender>female</gender>
|
6
|
+
<state>LA</state>
|
7
|
+
<city>Bon Temps</city>
|
8
|
+
<country>US</country>
|
9
|
+
<plusLevel>2</plusLevel>
|
10
|
+
<activeOwnedChallengeCount>0</activeOwnedChallengeCount>
|
11
|
+
</user>
|
12
|
+
<userTotals>
|
13
|
+
<totalDistance>52</totalDistance>
|
14
|
+
<totalDuration>10000000</totalDuration>
|
15
|
+
<totalRunsWithRoutes>0</totalRunsWithRoutes>
|
16
|
+
<totalRuns>5</totalRuns>
|
17
|
+
<totalCalories>10000</totalCalories>
|
18
|
+
<totalWorkouts>0</totalWorkouts>
|
19
|
+
<totalCardioDistance>0.0</totalCardioDistance>
|
20
|
+
<averageRunsPerWeek>1.0</averageRunsPerWeek>
|
21
|
+
<preferredRunDayOfWeek>Sunday </preferredRunDayOfWeek>
|
22
|
+
<pedoWorkouts>0</pedoWorkouts>
|
23
|
+
<totalSteps>0</totalSteps>
|
24
|
+
<longestStepcount>0</longestStepcount>
|
25
|
+
<averageStepcount>0.0</averageStepcount>
|
26
|
+
<caloriesPedometer>0</caloriesPedometer>
|
27
|
+
<totalCaloriesPedometer>0</totalCaloriesPedometer>
|
28
|
+
<totalRunsWithHeartrate>0</totalRunsWithHeartrate>
|
29
|
+
<previousSyncTime>2010-08-05T07:50:53+00:00</previousSyncTime>
|
30
|
+
<lastCalculated>2010-08-23T03:24:18+00:00</lastCalculated>
|
31
|
+
</userTotals>
|
32
|
+
<userOptions>
|
33
|
+
<screenName><![CDATA[sookie]]></screenName>
|
34
|
+
<distanceUnit>mi</distanceUnit>
|
35
|
+
<dateFormat>MM/DD/YY</dateFormat>
|
36
|
+
<startWeek>Su</startWeek>
|
37
|
+
<avatar>http://avatar.nikeplus.com/vampire.png</avatar>
|
38
|
+
<uploadedAvatar>http://avatar.nikeplus.com/vampire.png</uploadedAvatar>
|
39
|
+
<isPublic>true</isPublic>
|
40
|
+
<powerSong>
|
41
|
+
<artist><![CDATA[Jace Everett]]></artist>
|
42
|
+
<album><![CDATA[True Blood]]></album>
|
43
|
+
<title><![CDATA[Bad Things]]></title>
|
44
|
+
</powerSong>
|
45
|
+
<emailGoalEnding>false</emailGoalEnding>
|
46
|
+
<emailGoalComplete>false</emailGoalComplete>
|
47
|
+
<emailWeeklyTraining>false</emailWeeklyTraining>
|
48
|
+
<emailChallengeEnding>false</emailChallengeEnding>
|
49
|
+
<emailChallengeStarting>false</emailChallengeStarting>
|
50
|
+
<emailChallengeWinner>false</emailChallengeWinner>
|
51
|
+
</userOptions>
|
52
|
+
<mostRecentRun id="2" workoutType="standard">
|
53
|
+
<startTime>2010-08-04T19:37:47-07:00</startTime>
|
54
|
+
<distance>3.4539</distance>
|
55
|
+
<duration>976277</duration>
|
56
|
+
<workoutType>standard</workoutType>
|
57
|
+
<equipment>ipod</equipment>
|
58
|
+
</mostRecentRun>
|
59
|
+
</plusService>
|
data/test/helper.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'rr'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'redgreen'
|
7
|
+
require 'leftright'
|
8
|
+
rescue LoadError
|
9
|
+
end
|
10
|
+
|
11
|
+
dir = File.dirname(File.expand_path(__FILE__))
|
12
|
+
$LOAD_PATH.unshift(File.join(dir, '..', 'lib'))
|
13
|
+
$LOAD_PATH.unshift(dir)
|
14
|
+
|
15
|
+
require 'fatigue'
|
16
|
+
|
17
|
+
def fixture(file)
|
18
|
+
test_dir = File.dirname(File.expand_path(__FILE__))
|
19
|
+
file = File.join(test_dir,'fixtures',file)
|
20
|
+
File.read(file)
|
21
|
+
end
|
22
|
+
|
23
|
+
# test/spec/mini 3
|
24
|
+
# http://gist.github.com/25455
|
25
|
+
# chris@ozmm.org
|
26
|
+
# file:lib/test/spec/mini.rb
|
27
|
+
def context(*args, &block)
|
28
|
+
return super unless (name = args.first) && block
|
29
|
+
require 'test/unit'
|
30
|
+
klass = Class.new(defined?(ActiveSupport::TestCase) ? ActiveSupport::TestCase : Test::Unit::TestCase) do
|
31
|
+
include RR::Adapters::TestUnit
|
32
|
+
|
33
|
+
def self.test(name, &block)
|
34
|
+
define_method("test_#{name.gsub(/\W/,'_')}", &block) if block
|
35
|
+
end
|
36
|
+
def self.xtest(*args) end
|
37
|
+
def self.setup(&block) define_method(:setup, &block) end
|
38
|
+
def self.teardown(&block) define_method(:teardown, &block) end
|
39
|
+
end
|
40
|
+
(class << klass; self end).send(:define_method, :name) { name.gsub(/\W/,'_') }
|
41
|
+
klass.class_eval &block
|
42
|
+
end
|
data/test/test_garmin.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), *%w[helper])
|
2
|
+
|
3
|
+
context "Garmin" do
|
4
|
+
|
5
|
+
setup do
|
6
|
+
@garmin = Fatigue::Garmin.new('username','password')
|
7
|
+
end
|
8
|
+
|
9
|
+
test "verify response" do
|
10
|
+
error = @garmin.verify_response('<span id="ErrorContainer">An Error</span>')
|
11
|
+
assert_equal false, error
|
12
|
+
|
13
|
+
error = @garmin.verify_response('<span id="ErrorContainer"></span>')
|
14
|
+
assert_equal true, error
|
15
|
+
|
16
|
+
error = @garmin.verify_response('')
|
17
|
+
assert_equal true, error
|
18
|
+
end
|
19
|
+
end
|
data/test/test_nike.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), *%w[helper])
|
2
|
+
|
3
|
+
context "Nike" do
|
4
|
+
|
5
|
+
setup do
|
6
|
+
@nike = Fatigue::Nike.new(1)
|
7
|
+
end
|
8
|
+
|
9
|
+
test "runs" do
|
10
|
+
mock(@nike).run_list_xml { fixture('runs.xml') }
|
11
|
+
#mock(@nike).user_data_xml { fixture('user.xml') }
|
12
|
+
runs = @nike.runs
|
13
|
+
|
14
|
+
assert_equal runs.size, 2
|
15
|
+
assert_equal runs.first.distance, '3'
|
16
|
+
assert_equal runs.last.duration, 1000000
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
data/test/test_run.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), *%w[helper])
|
2
|
+
|
3
|
+
context "Run" do
|
4
|
+
|
5
|
+
setup do
|
6
|
+
@run = Fatigue::Run.new
|
7
|
+
end
|
8
|
+
|
9
|
+
test "basic setters" do
|
10
|
+
@run.duration = 5
|
11
|
+
assert_equal 5, @run.duration
|
12
|
+
|
13
|
+
@run.calories = 100
|
14
|
+
assert_equal 100, @run.calories
|
15
|
+
end
|
16
|
+
|
17
|
+
test "date conversion" do
|
18
|
+
@run.started_at = '2009-03-17T04:02:43+00:00'
|
19
|
+
assert_equal @run.started_at.to_s, 'Tue Mar 17 04:02:43 UTC 2009'
|
20
|
+
end
|
21
|
+
|
22
|
+
test "formatted name" do
|
23
|
+
@run.service = 'Nike+'
|
24
|
+
@run.name = 'I ran this while drunk'
|
25
|
+
assert_equal @run.formatted_name, 'Nike+: I ran this while drunk'
|
26
|
+
end
|
27
|
+
|
28
|
+
test "seconds" do
|
29
|
+
@run.duration = '1000000'
|
30
|
+
assert_equal 40, @run.seconds
|
31
|
+
end
|
32
|
+
|
33
|
+
test "minutes" do
|
34
|
+
@run.duration = 1000000
|
35
|
+
assert_equal 16, @run.minutes
|
36
|
+
end
|
37
|
+
|
38
|
+
test "hours" do
|
39
|
+
@run.duration = 5000000
|
40
|
+
assert_equal 1, @run.hours
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
metadata
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fatigue
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Zach Holman
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-09-12 00:00:00 -07:00
|
19
|
+
default_executable: fatigue
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: nokogiri
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 7
|
30
|
+
segments:
|
31
|
+
- 1
|
32
|
+
- 4
|
33
|
+
- 0
|
34
|
+
version: 1.4.0
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: mechanize
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - "="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 23
|
46
|
+
segments:
|
47
|
+
- 1
|
48
|
+
- 0
|
49
|
+
- 0
|
50
|
+
version: 1.0.0
|
51
|
+
type: :runtime
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: ruby-progressbar
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - "="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 13
|
62
|
+
segments:
|
63
|
+
- 0
|
64
|
+
- 0
|
65
|
+
- 9
|
66
|
+
version: 0.0.9
|
67
|
+
type: :runtime
|
68
|
+
version_requirements: *id003
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rr
|
71
|
+
prerelease: false
|
72
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - "="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
hash: 33
|
78
|
+
segments:
|
79
|
+
- 0
|
80
|
+
- 10
|
81
|
+
- 11
|
82
|
+
version: 0.10.11
|
83
|
+
type: :development
|
84
|
+
version_requirements: *id004
|
85
|
+
description: |-
|
86
|
+
A tiny little gem that helps you load your Nike+ run history
|
87
|
+
into Garmin Connect. Cool, huh?.
|
88
|
+
email: fatigue@zachholman.com
|
89
|
+
executables:
|
90
|
+
- fatigue
|
91
|
+
extensions: []
|
92
|
+
|
93
|
+
extra_rdoc_files:
|
94
|
+
- readme.markdown
|
95
|
+
files:
|
96
|
+
- Rakefile
|
97
|
+
- bin/fatigue
|
98
|
+
- fatigue.gemspec
|
99
|
+
- lib/fatigue.rb
|
100
|
+
- lib/fatigue/garmin.rb
|
101
|
+
- lib/fatigue/nike.rb
|
102
|
+
- lib/fatigue/run.rb
|
103
|
+
- readme.markdown
|
104
|
+
- test/fixtures/runs.xml
|
105
|
+
- test/fixtures/user.xml
|
106
|
+
- test/helper.rb
|
107
|
+
- test/test_garmin.rb
|
108
|
+
- test/test_nike.rb
|
109
|
+
- test/test_run.rb
|
110
|
+
has_rdoc: true
|
111
|
+
homepage: http://github.com/holman/fatigue
|
112
|
+
licenses: []
|
113
|
+
|
114
|
+
post_install_message:
|
115
|
+
rdoc_options:
|
116
|
+
- --charset=UTF-8
|
117
|
+
require_paths:
|
118
|
+
- lib
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
120
|
+
none: false
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
hash: 3
|
125
|
+
segments:
|
126
|
+
- 0
|
127
|
+
version: "0"
|
128
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
hash: 3
|
134
|
+
segments:
|
135
|
+
- 0
|
136
|
+
version: "0"
|
137
|
+
requirements: []
|
138
|
+
|
139
|
+
rubyforge_project: fatigue
|
140
|
+
rubygems_version: 1.3.7
|
141
|
+
signing_key:
|
142
|
+
specification_version: 2
|
143
|
+
summary: Import your Nike+ runs into Garmin Connect.
|
144
|
+
test_files:
|
145
|
+
- test/test_garmin.rb
|
146
|
+
- test/test_nike.rb
|
147
|
+
- test/test_run.rb
|