fatigue 0.0.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.
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,12 @@
1
+ require 'fatigue/garmin'
2
+ require 'fatigue/nike'
3
+ require 'fatigue/run'
4
+
5
+ require 'nokogiri'
6
+ require 'open-uri'
7
+ require 'mechanize'
8
+ require 'progressbar'
9
+
10
+ module Fatigue
11
+ VERSION = '0.0.1'
12
+ end
@@ -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
@@ -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
@@ -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
@@ -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