nike 0.2.2

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/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in nike.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Alex Skryl
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # Nike
2
+
3
+ A Ruby client for the Nike+ API
4
+
5
+ ## Features
6
+
7
+ * Run Stats
8
+ * Heart Rate Stats
9
+ * GPS Data
10
+ * Automatic Unit Conversion
11
+ * Data Set Caching
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ gem 'nike'
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install nike
26
+
27
+ ## Basic Usage
28
+
29
+ Initialize the client
30
+
31
+ $ c = Nike::Client.new('your_email', 'your_password')
32
+
33
+ A summary of all activities by type (type is :run by default)
34
+
35
+ $ c.activities # get all runs
36
+
37
+ $ c.activities(type: :hr) # get all heart rate activities
38
+
39
+ Full activity data (Slow if you have alot of data, use c.activity to fetch a detailed data set for a single activity)
40
+
41
+ $ c.detailed_activities # get detailed data for all runs
42
+
43
+ $ c.detailed_activities(type: :hr) # get detailed data for all hr activity
44
+
45
+ Detailed data for a single activity (The id can be found in using the summary calls above)
46
+
47
+ $ c.activity(id)
48
+
49
+ Lifetime stats
50
+
51
+ $ c.lifetime_totals # lifetime running totals
52
+
53
+ $ c.lifetime_totals(type: :hr) # lifetime hr totals
54
+
55
+ More metrics
56
+
57
+ $ c.time_span_metrics # run metrics
58
+
59
+ $ c.time_span_metrics # hr metrics
60
+
61
+ Distance by time of day
62
+
63
+ $ c.time_of_day_metrics
64
+
65
+ Distance by terrain
66
+
67
+ $ c.terrains
68
+
69
+ Pace data
70
+
71
+ $ c.paces
72
+
73
+ Basic stats that appear on the Nike+ homepage
74
+
75
+ $ c.homepage_stats
76
+
77
+ ## GPS Data
78
+
79
+ Cheking if an activity includes GPS data
80
+
81
+ $ a = @c.activity(#######)
82
+
83
+ $ a.gps # => true
84
+
85
+ Getting to the GPS data
86
+
87
+ $ a.geo.waypoints # list all GPS waypoints
88
+
89
+ $ a.geo.waypoints.first # => {"lat"=>42.115833, "lon"=>-87.776344, "ele"=>181.78954}
90
+ $ a.geo.waypoints.first.lat # => 42.115833
91
+
92
+ ## Automatic Unit Conversion
93
+
94
+ Check to see which conversion helpers are available for a specific data set
95
+
96
+ $ a = @c.activity(#######)
97
+
98
+ $ a.conversion_helpers # => [:distance_in_kilometers, :distance_in_miles, :duration_inseconds,
99
+ :duration_in_minutes, :duration_in_hours, :duration_in_hms,
100
+ :speed_in_mph, :speed_in_kph]
101
+
102
+ Examples
103
+
104
+ $ a.duration # => 6402672
105
+ $ a.duration_in_seconds # => 6402.672
106
+ $ a.duration_in_minutes # => 106.7112
107
+ $ a.duration_in_hours # => 1.77852
108
+ $ a.duration_in_hms # => "01:46:42"
109
+
110
+ All time fields are automatically converted to Ruby Time objects
111
+
112
+ $ a.start_time_utc.class # => Time
113
+
114
+ ## Caching
115
+
116
+ Toggle caching during client initialization
117
+
118
+ $ c = Nike::Client.new('your_email', 'your_password', caching: true)
119
+
120
+ Toggle caching after client initialization
121
+
122
+ $ c.caching = false
123
+
124
+ Perform the HTTP request for a particular call even if caching is enabled. This
125
+ will have the side-effect of refreshing the cache.
126
+
127
+ $ c.activities!
128
+
129
+
130
+ ## Contributing
131
+
132
+ 1. Fork it
133
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
134
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
135
+ 4. Push to the branch (`git push origin my-new-feature`)
136
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,135 @@
1
+ require_relative 'core_ext/hash.rb'
2
+
3
+ require 'httparty'
4
+ require 'hashie'
5
+ require 'active_support/core_ext/string/inflections'
6
+
7
+ class Nike::Client
8
+ include HTTParty
9
+ # debug_output $stdout
10
+
11
+ LOGIN_URL = 'https://secure-nikeplus.nike.com/nsl/services/user/login'
12
+ BASE_URL = 'http://nikeplus.nike.com/plus'
13
+ APP_KEY = 'b31990e7-8583-4251-808f-9dc67b40f5d2'
14
+ FORMAT = :json
15
+
16
+ # service urls
17
+ #
18
+ RUN_ACTIVITIES_URL = '/activity/running/[user_id]/lifetime/activities'
19
+ HR_ACTIVITIES_URL = '/activity/running/[user_id]/heartrate/lifetime/activities'
20
+ ACTIVITY_URL = '/running/ajax'
21
+
22
+ ACTIVITIES_URLS = {
23
+ run: RUN_ACTIVITIES_URL,
24
+ hr: HR_ACTIVITIES_URL
25
+ }
26
+
27
+ format FORMAT
28
+ base_uri BASE_URL
29
+ default_params format: FORMAT, app: APP_KEY
30
+
31
+ attr_accessor :caching
32
+
33
+ def initialize(email, password, opts = {})
34
+ @email, @password, @user_id = email, password, nil
35
+ @caching, @cache = opts[:caching] || true, {}
36
+ end
37
+
38
+ def activity(id)
39
+ fetch_activity_data(id.to_s).activity
40
+ end
41
+
42
+ def activities(opts = {})
43
+ fetch_user_data(opts).activities.map { |a| a.activity }
44
+ end
45
+
46
+ def detailed_activities(opts = {})
47
+ activities(opts).map { |a| activity(a.activity_id) }
48
+ end
49
+
50
+ [:lifetime_totals, :time_span_metrics, :time_of_day_metrics, :terrains, :paces, :homepage_stats].each do |m|
51
+ eval %(
52
+ def #{m}(opts = {})
53
+ fetch_user_data(opts).send(:#{m})
54
+ end
55
+ )
56
+ end
57
+
58
+ def method_missing(method_name, *args, &block)
59
+ if /(.*)!$/ === method_name && self.respond_to?(method_name.to_s.chop)
60
+ no_cache { self.send($1, *args, &block) }
61
+ else super
62
+ end
63
+ end
64
+
65
+ def respond_to_missing?(method_name, include_private = false)
66
+ method_name.to_s.end_with?('!') && self.respond_to?(method_name.to_s.chop)
67
+ end
68
+
69
+ private
70
+
71
+ # data
72
+
73
+ def fetch_activity_data(id)
74
+ cache(id) do
75
+ wrap get_authorized(ACTIVITY_URL + "/#{id}")
76
+ end
77
+ end
78
+
79
+ def fetch_user_data(opts)
80
+ type = opts[:type].to_sym || :run
81
+ cache(type) do
82
+ wrap get_authorized(ACTIVITIES_URLS[type], query: { indexStart: 0, indexEnd: 999999})
83
+ end
84
+ end
85
+
86
+ # auth
87
+
88
+ def get_authorized(url, opts = {})
89
+ login_if_unauthenticated
90
+ raise "Authentication failed!" unless logged_in?
91
+ self.class.get(personify_url(url), opts).to_hash
92
+ end
93
+
94
+ def login_if_unauthenticated
95
+ return if logged_in?
96
+ response = self.class.login(@email, @password)
97
+ @user_id = response['serviceResponse']['body']['User']['screenName']
98
+ end
99
+
100
+ def self.login(email, password)
101
+ response = post(LOGIN_URL, query: { email: email, password: password })
102
+ self.default_cookies.add_cookies(response.headers['set-cookie'])
103
+ response
104
+ end
105
+
106
+ def logged_in?
107
+ !@user_id.nil? && !self.class.cookies[:slCheck].nil?
108
+ end
109
+
110
+ # caching
111
+
112
+ def cache(key)
113
+ @caching ? @cache[key] ||= yield : yield
114
+ end
115
+
116
+ def no_cache
117
+ caching = @caching
118
+ @caching = false
119
+ yield
120
+ ensure
121
+ @caching = caching
122
+ end
123
+
124
+ # helpers
125
+
126
+ def personify_url(url)
127
+ vars = url.scan(/\[[^\]]*\]/)
128
+ vars.inject(url){ |u, v| u.gsub(v, self.instance_variable_get("@#{v[1..-2]}").to_s) }
129
+ end
130
+
131
+ def wrap(response)
132
+ Nike::Mash.new(response.underscore_keys)
133
+ end
134
+
135
+ end
@@ -0,0 +1,34 @@
1
+ class Hash
2
+
3
+ def deep_merge(other_hash)
4
+ dup.deep_merge!(other_hash)
5
+ end
6
+
7
+ def deep_merge!(other_hash)
8
+ other_hash.each_pair do |k,v|
9
+ tv = self[k]
10
+ self[k] = \
11
+ if tv.is_a?(Hash) && v.is_a?(Hash)
12
+ tv.deep_merge(v)
13
+ elsif tv.is_a?(Array) && v.is_a?(Array)
14
+ tv + v
15
+ else v
16
+ end
17
+ end
18
+ self
19
+ end
20
+
21
+ def underscore_keys
22
+ self.inject({}) do |h, (k,v)|
23
+ h[k.to_s.underscore] = \
24
+ case v
25
+ when Hash
26
+ v.underscore_keys
27
+ when Array
28
+ v.map { |i| i.is_a?(Hash) ? i.underscore_keys : i }
29
+ else v
30
+ end; h
31
+ end
32
+ end
33
+
34
+ end
data/lib/nike/mash.rb ADDED
@@ -0,0 +1,99 @@
1
+ require 'hashie'
2
+
3
+ class Nike::Mash < Hashie::Mash
4
+ KM_TO_MILE = 0.621371192
5
+
6
+ CONVERTER_AVAILABILITY = {
7
+ distance: [:distance_in_kilometers, :distance_in_miles, :speed_in_mph, :speed_in_kph],
8
+ duration: [:duration_in_seconds, :duration_in_minutes, :duration_in_hours, :duration_in_hms],
9
+ pace: [:pace_in_mpk, :pace_in_mpm]
10
+ }
11
+
12
+ CONVERTER_LOOKUP = {
13
+ distance_in_kilometers: :distance_to_kilometers,
14
+ distance_in_miles: :distance_to_miles,
15
+ duration_in_seconds: :duration_to_seconds,
16
+ duration_in_minutes: :duration_to_minutes,
17
+ duration_in_hours: :duration_to_hours,
18
+ duration_in_hms: :duration_to_hms,
19
+ speed_in_mph: :speed_to_mph,
20
+ speed_in_kph: :speed_to_kph,
21
+ pace_in_mpk: :pace_to_mpk,
22
+ pace_in_mpm: :pace_to_mpm,
23
+ }
24
+
25
+ def method_missing(method, *args, &block)
26
+ case method.to_s
27
+ when /_time$|_time_utc$/
28
+ Time.parse(super)
29
+ else
30
+ converter = CONVERTER_LOOKUP[method]
31
+ converter ? self.send(converter) : super
32
+ end
33
+ end
34
+
35
+ def respond_to_missing?(method, include_private = false)
36
+ conversion_helpers.include?(method)
37
+ end
38
+
39
+ def conversion_helpers
40
+ CONVERTER_AVAILABILITY.inject([]) do |a, (m, converters)|
41
+ self.include?(m.to_s) ? a += converters : a
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ # distance
48
+
49
+ def distance_to_kilometers
50
+ distance
51
+ end
52
+
53
+ def distance_to_miles
54
+ distance * KM_TO_MILE
55
+ end
56
+
57
+ # duration
58
+
59
+ def duration_to_seconds
60
+ duration.to_f / 1000
61
+ end
62
+
63
+ def duration_to_minutes
64
+ duration.to_f / 60000
65
+ end
66
+
67
+ def duration_to_hours
68
+ duration.to_f / 3600000
69
+ end
70
+
71
+ def duration_to_hms
72
+ Time.at(duration_to_seconds).gmtime.strftime('%R:%S')
73
+ end
74
+
75
+ # speed
76
+
77
+ def speed_to_mph
78
+ distance_in_miles && duration_in_hours &&
79
+ distance_in_miles / duration_in_hours
80
+ end
81
+
82
+ def speed_to_kph
83
+ distance_in_kilometers && duration_in_hours &&
84
+ distance_in_kilometers / duration_in_hours
85
+ end
86
+
87
+ # speed
88
+
89
+ def pace_to_mpk
90
+ distnace_in_kilometers && duration_in_minutes &&
91
+ duration_in_minutes / distance_in_kilometers
92
+ end
93
+
94
+ def pace_to_mpm
95
+ distnace_in_miles && duration_in_minutes &&
96
+ duration_in_minutes / distance_in_miles
97
+ end
98
+
99
+ end
@@ -0,0 +1,3 @@
1
+ module Nike
2
+ VERSION = "0.2.2"
3
+ end
data/lib/nike.rb ADDED
@@ -0,0 +1,5 @@
1
+ module Nike; end
2
+
3
+ require 'nike/version'
4
+ require 'nike/mash'
5
+ require 'nike/client'
data/nike.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'nike/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "nike"
8
+ gem.version = Nike::VERSION
9
+ gem.authors = ["Alex Skryl"]
10
+ gem.email = ["rut216@gmail.com"]
11
+ gem.summary = %q{A Ruby client for the Nike+ API}
12
+ gem.description = %q{A Ruby client for the Nike+ API with support for Run/GPS/HR Data}
13
+
14
+ gem.files = `git ls-files`.split($/)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.require_paths = ["lib"]
18
+
19
+ gem.add_dependency(%q<hashie>, [">= 1.2.0"])
20
+ gem.add_dependency(%q<activesupport>, [">= 3.2.0"])
21
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nike
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Alex Skryl
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-08-13 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: hashie
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 1.2.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 1.2.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: activesupport
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: 3.2.0
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: 3.2.0
46
+ description: A Ruby client for the Nike+ API with support for Run/GPS/HR Data
47
+ email:
48
+ - rut216@gmail.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - .gitignore
54
+ - Gemfile
55
+ - LICENSE.txt
56
+ - README.md
57
+ - Rakefile
58
+ - lib/nike.rb
59
+ - lib/nike/client.rb
60
+ - lib/nike/core_ext/hash.rb
61
+ - lib/nike/mash.rb
62
+ - lib/nike/version.rb
63
+ - nike.gemspec
64
+ homepage:
65
+ licenses: []
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ! '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ requirements: []
83
+ rubyforge_project:
84
+ rubygems_version: 1.8.23
85
+ signing_key:
86
+ specification_version: 3
87
+ summary: A Ruby client for the Nike+ API
88
+ test_files: []
89
+ has_rdoc: