rscrobbler 0.2.2 → 0.3.0

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/lib/lastfm/venue.rb CHANGED
@@ -1,25 +1,60 @@
1
- module LastFM
2
- class Venue < Struct.new(:id, :name, :location, :url, :website, :phone_number, :images)
3
-
4
- def update_from_node(node)
5
- case node.name.to_sym
6
- when :id
7
- self.id = node.content.to_i
8
- when :name
9
- self.name = node.content
10
- when :location
11
- # ??? city, country, street, postalcode, geo:lat, geo:long
12
- when :url
13
- self.url = node.content
14
- when :website
15
- self.website = node.content
16
- when :phoneNumber
17
- self.phone_number = node.content
18
- when :image
19
- self.images ||= {}
20
- self.images.merge!({node['size'].to_sym => node.content})
21
- end
22
- end
23
-
24
- end
25
- end
1
+ module LastFM
2
+ class Venue < Struct.new(:id, :name, :location, :url, :website, :phone_number, :images)
3
+
4
+ def update_from_node(node)
5
+ case node.name.to_sym
6
+ when :id
7
+ self.id = node.content.to_i
8
+ when :name
9
+ self.name = node.content
10
+ when :location
11
+ # ??? city, country, street, postalcode, geo:lat, geo:long
12
+ when :url
13
+ self.url = node.content
14
+ when :website
15
+ self.website = node.content
16
+ when :phoneNumber
17
+ self.phone_number = node.content
18
+ when :image
19
+ self.images ||= {}
20
+ self.images.merge!({node['size'].to_sym => node.content})
21
+ end
22
+ end
23
+
24
+ # API Methods
25
+ class << self
26
+
27
+ # Get a list of upcoming events for a venue.
28
+ #
29
+ # @option params [Fixnum, required] :venue the id for the venue to fetch event listings for
30
+ # @option params [Boolean, optional] :festivalsonly whether only festivals should be returned, or all events
31
+ # @see http://www.last.fm/api/show?service=394
32
+ def get_events( params )
33
+ LastFM.get( "venue.getEvents", params )
34
+ end
35
+
36
+ # Get a paginated list of all the events held at this venue in the past.
37
+ #
38
+ # @option params [Fixnum, required] :venue the id for the venue to fetch event listings for
39
+ # @option params [Boolean, optional] :festivalsonly whether only festivals should be returned, or all events
40
+ # @option params [Fixnum, optional] :page the page number to fetch. defaults to first page
41
+ # @option params [Fixnum, optional] :limit the number of results to fetch per page. defaults to 50
42
+ # @see http://www.last.fm/api/show?service=395
43
+ def get_past_events( params )
44
+ LastFM.get( "venue.getPastEvents", params )
45
+ end
46
+
47
+ # Search for a venue by venue name.
48
+ #
49
+ # @option params [String, required] :venue the venue name to search for
50
+ # @option params [String, optional] :country a country name used to limit results, as defined by ISO 3166-1
51
+ # @option params [Fixnum, optional] :page the page number to fetch. defaults to first page
52
+ # @option params [Fixnum, optional] :limit the number of results to fetch per page. defaults to 50
53
+ # @see http://www.last.fm/api/show?service=396
54
+ def search( params )
55
+ LastFM.get( "venue.search", params )
56
+ end
57
+
58
+ end
59
+ end
60
+ end
data/lib/lastfm/wiki.rb CHANGED
@@ -1,20 +1,20 @@
1
- module LastFM
2
-
3
- # @attr [Time] published Date the information in this entry was published
4
- # @attr [String] summary Short summary of the information in this entry
5
- # @attr [String] content Full content of this entry
6
- class Wiki < Struct.new(:published, :summary, :content)
7
-
8
- def update_from_node(node)
9
- case node.name.to_sym
10
- when :published
11
- self.published = Time.parse(node.content) rescue nil
12
- when :summary #TODO: Remove CDATA wrapper
13
- self.summary = node.content
14
- when :content #TODO: Remove CDATA wrapper
15
- self.content = node.content
16
- end
17
- end
18
-
19
- end
20
- end
1
+ module LastFM
2
+
3
+ # @attr [Time] published Date the information in this entry was published
4
+ # @attr [String] summary Short summary of the information in this entry
5
+ # @attr [String] content Full content of this entry
6
+ class Wiki < Struct.new(:published, :summary, :content)
7
+
8
+ def update_from_node(node)
9
+ case node.name.to_sym
10
+ when :published
11
+ self.published = Time.parse(node.content) rescue nil
12
+ when :summary #TODO: Remove CDATA wrapper
13
+ self.summary = node.content
14
+ when :content #TODO: Remove CDATA wrapper
15
+ self.content = node.content
16
+ end
17
+ end
18
+
19
+ end
20
+ end
data/lib/rscrobbler.rb CHANGED
@@ -1,214 +1,207 @@
1
- require 'digest/md5'
2
- require 'json'
3
- require 'libxml'
4
- require 'net/http'
5
- require 'time'
6
- require 'uri'
7
-
8
- $:.unshift(File.dirname(__FILE__))
9
-
10
- require 'lastfm/struct'
11
-
12
- require 'lastfm/album'
13
- require 'lastfm/artist'
14
- require 'lastfm/buylink'
15
- require 'lastfm/event'
16
- require 'lastfm/shout'
17
- require 'lastfm/tag'
18
- require 'lastfm/track'
19
- require 'lastfm/venue'
20
- require 'lastfm/wiki'
21
-
22
- require 'lastfm/api/album'
23
- require 'lastfm/api/artist'
24
- require 'lastfm/api/auth'
25
- require 'lastfm/api/chart'
26
- require 'lastfm/api/event'
27
- require 'lastfm/api/geo'
28
- require 'lastfm/api/group'
29
- require 'lastfm/api/library'
30
- require 'lastfm/api/playlist'
31
- require 'lastfm/api/radio'
32
- require 'lastfm/api/tag'
33
- require 'lastfm/api/tasteometer'
34
- require 'lastfm/api/track'
35
- require 'lastfm/api/user'
36
- require 'lastfm/api/venue'
37
-
38
- module LastFM
39
- VERSION = '0.2.2'
40
-
41
- HOST = 'ws.audioscrobbler.com'
42
- API_VERSION = '2.0'
43
-
44
- class LastFMError < StandardError; end
45
- class AuthenticationError < StandardError; end
46
-
47
- class << self
48
- attr_accessor :api_key, :api_secret, :username, :auth_token, :session_key, :logger
49
-
50
- # Configure the module and begin a session. Once established (and successfully
51
- # executed), the module is ready to send api calls to Last.fm.
52
- #
53
- # @example
54
- # LastFM.establish_session do |session|
55
- # session.username = ...
56
- # session.auth_token = ...
57
- # session.api_key = ...
58
- # session.api_secret = ...
59
- # end
60
- #
61
- # @param [Block] &block block used to configure the module's attributes
62
- # @return [String] session key if successfully connected
63
- # @raise [AuthenticationError] if any authentication attributes are missing
64
- # @raise [LastFMError] if the HTTP request to Last.fm returns an error
65
- def establish_session(&block)
66
- yield self
67
- self.authenticate!
68
- end
69
-
70
- # Authenticate the service with provided login credentials. Use mobile
71
- # authentication to avoid redirecting to the website to log in.
72
- #
73
- # @see http://www.last.fm/api/authspec last.fm auth spec
74
- # @return [String] session key provided from authentication
75
- def authenticate!
76
- [:api_key, :api_secret, :username, :auth_token].each do |cred|
77
- raise AuthenticationError, "Missing credential: #{cred}" unless LastFM.send(cred)
78
- end
79
- self.session_key = Api::Auth.get_mobile_session( username: username, auth_token: auth_token ).find_first('session/key').content
80
- end
81
-
82
- # Has the service been authenticated?
83
- #
84
- # @return [Boolean] whether the service has been authenticated
85
- def authenticated?
86
- !!session_key
87
- end
88
-
89
- # Ensure the service has been authenticated; raise an error if it hasn't.
90
- #
91
- # @raise [AuthenticationError] if the service hasn't been authenticated.
92
- def requires_authentication
93
- raise AuthenticationError, 'LastFM Authentication Required' unless authenticated?
94
- end
95
-
96
- # Generate auth token from username and given password.
97
- #
98
- # @param [String] password password to use
99
- # @return [String] md5 digest of the username and password
100
- def generate_auth_token( password )
101
- self.auth_token = Digest::MD5.hexdigest( username.dup << Digest::MD5.hexdigest(password) )
102
- end
103
-
104
- # Construct an HTTP GET call from params, and load the response into a LibXML Document.
105
- #
106
- # @param [String] method last.fm api method to call
107
- # @param [Boolean] secure whether to sign the request with a method signature and session key
108
- # (one exception being auth methods, which require a method signature but no session key)
109
- # @param [Hash] params parameters to send, excluding method, api_key, api_sig, and sk
110
- # @return [LibXML::XML::Document] xml document of the data contained in the response
111
- # @raise [LastFMError] if the request fails
112
- def get( method, params = {}, secure = false )
113
- path = generate_path(method, secure, params)
114
- logger.debug( "Last.fm HTTP GET: #{HOST}#{path}" ) if logger
115
- response = Net::HTTP.get_response( HOST, path )
116
- validate( LibXML::XML::Parser.string( response.body ).parse )
117
- end
118
-
119
- # Construct an HTTP POST call from params, and check the response status.
120
- #
121
- # @param [String] method last.fm api method to call
122
- # @param [Hash] params parameters to send, excluding method, api_key, api_sig, and sk
123
- # @return [LibXML::XML::Document] xml document of the data contained in the response
124
- # @raise [LastFMError] if the request fails
125
- def post( method, params )
126
- post_uri = URI.parse("http://#{HOST}/#{API_VERSION}/")
127
- params = construct_params( method, :secure, params )
128
- logger.debug( "Last.fm HTTP POST: #{post_uri}, #{params.to_s}" ) if logger
129
- response = Net::HTTP.post_form( post_uri, params )
130
- validate( LibXML::XML::Parser.string( response.body ).parse )
131
- end
132
-
133
- private
134
-
135
- # Check an XML document for status = failed, and throw a descriptive error if it's found.
136
- #
137
- # @param [LibXML::XML::Document] xml xml document to check for errors
138
- # @return [LibXML::XML::Document] the xml document if no errors were found
139
- # @raise [LastFMError] if an error is found
140
- # @private
141
- def validate( xml )
142
- raise LastFMError, xml.find_first('error').content if xml.root.attributes['status'] == 'failed'
143
- xml
144
- end
145
-
146
- # Normalize the parameter list by converting boolean values to 0 or 1, array values to
147
- # comma-separated strings, and all other values to a string. Remove any nil values, and
148
- # camel-case the parameter keys. Add method, api key, session key, and api signature
149
- # parameters where necessary.
150
- #
151
- # @param [String] method last.fm api method
152
- # @param [Boolean] secure whether to include session key and api signature in the parameters
153
- # @param [Hash] params parameters to normalize and add to
154
- # @return [Hash] complete, normalized parameters
155
- # @private
156
- def construct_params( method, secure, params )
157
- params = params.each_with_object({}) do |(k,v), h|
158
- v = v ? 1 : 0 if !!v == v # convert booleans into 0 or 1
159
- v = v.compact.join(',') if v.is_a?(Array) # convert arrays into comma-separated strings
160
- v = v.to_i if v.is_a?(Time) # convert times into integer unix timestamps
161
- h[camel_case(k)] = v.to_s unless v.nil?
162
- end
163
- params['method'] = method
164
- params['api_key'] = api_key
165
- params['sk'] = session_key if authenticated? && secure
166
- params['api_sig'] = generate_method_signature( params ) if secure
167
- params
168
- end
169
-
170
- # Return a the camelCased version of the given string or symbol, with underscores removed,
171
- # word capitalized, and the first letter lower case.
172
- #
173
- # @param [String] str the string (or symbol) to camel case
174
- # @return [String] the camelcased version of the given string
175
- def camel_case(key)
176
- exceptions = {playlist_id: 'playlistID', playlist_url: 'playlistURL',
177
- speed_multiplier: 'speed_multiplier', fingerprint_id: 'fingerprintid'}
178
- return exceptions[key] if exceptions.include?(key)
179
- camel = key.to_s.split('_').map{|s| s.capitalize}.join
180
- camel[0].downcase + camel[1..-1]
181
- end
182
-
183
- # Generate the path for a particular method call given params.
184
- #
185
- # @param [String] method last.fm method to call
186
- # @param [Boolean] secure whether to include session key and api signature in the call
187
- # @param [optional, Hash] params parameters to include in the api call
188
- # @return [String] path for the api call
189
- # @private
190
- def generate_path( method, secure, params={} )
191
- params = construct_params( method, secure, params )
192
- url = "/#{API_VERSION}/?method=#{params.delete('method')}"
193
- params.keys.each do |key|
194
- url << "&#{key}=#{params[key]}"
195
- end
196
- URI.encode(url)
197
- end
198
-
199
- # Generate a method signature based on given parameters.
200
- #
201
- # @param [Hash] parameters to combine into a method signature
202
- # @return [String] method signature based on all the parameters
203
- # @see http://www.last.fm/api/authspec#8
204
- # @private
205
- def generate_method_signature( params )
206
- str = ''
207
- params.keys.sort.each do |key|
208
- str << key << params[key]
209
- end
210
- Digest::MD5.hexdigest( str << api_secret )
211
- end
212
-
213
- end
214
- end
1
+ require 'cgi'
2
+ require 'digest/md5'
3
+ require 'json'
4
+ require 'libxml'
5
+ require 'net/http'
6
+ require 'time'
7
+ require 'uri'
8
+
9
+ $:.unshift(File.dirname(__FILE__))
10
+
11
+ [
12
+ :struct,
13
+ :album,
14
+ :artist,
15
+ :auth,
16
+ :buylink,
17
+ :chart,
18
+ :event,
19
+ :geo,
20
+ :group,
21
+ :library,
22
+ :playlist,
23
+ :radio,
24
+ :shout,
25
+ :tag,
26
+ :tasteometer,
27
+ :track,
28
+ :user,
29
+ :venue,
30
+ :wiki
31
+ ].each{|model| require "lastfm/#{model}"}
32
+
33
+ module LastFM
34
+ VERSION = '0.3.0'
35
+
36
+ HOST = 'ws.audioscrobbler.com'
37
+ API_VERSION = '2.0'
38
+
39
+ class LastFMError < StandardError; end
40
+ class AuthenticationError < StandardError; end
41
+
42
+ class << self
43
+ attr_accessor :api_key, :api_secret, :username, :auth_token, :session_key, :logger
44
+
45
+ # Configure the module and begin a session. Once established (and successfully
46
+ # executed), the module is ready to send api calls to Last.fm.
47
+ #
48
+ # @example
49
+ # LastFM.establish_session do |session|
50
+ # session.username = ...
51
+ # session.auth_token = ...
52
+ # session.api_key = ...
53
+ # session.api_secret = ...
54
+ # end
55
+ #
56
+ # @param [Block] &block block used to configure the module's attributes
57
+ # @return [String] session key if successfully connected
58
+ # @raise [AuthenticationError] if any authentication attributes are missing
59
+ # @raise [LastFMError] if the HTTP request to Last.fm returns an error
60
+ def establish_session(&block)
61
+ yield self
62
+ self.authenticate!
63
+ end
64
+
65
+ # Authenticate the service with provided login credentials. Use mobile
66
+ # authentication to avoid redirecting to the website to log in.
67
+ #
68
+ # @see http://www.last.fm/api/authspec last.fm auth spec
69
+ # @return [String] session key provided from authentication
70
+ def authenticate!
71
+ [:api_key, :api_secret, :username, :auth_token].each do |cred|
72
+ raise AuthenticationError, "Missing credential: #{cred}" unless LastFM.send(cred)
73
+ end
74
+ self.session_key = Auth.get_mobile_session( username: username, auth_token: auth_token ).find_first('session/key').content
75
+ end
76
+
77
+ # Has the service been authenticated?
78
+ #
79
+ # @return [Boolean] whether the service has been authenticated
80
+ def authenticated?
81
+ !!session_key
82
+ end
83
+
84
+ # Ensure the service has been authenticated; raise an error if it hasn't.
85
+ #
86
+ # @raise [AuthenticationError] if the service hasn't been authenticated.
87
+ def requires_authentication
88
+ raise AuthenticationError, 'LastFM Authentication Required' unless authenticated?
89
+ end
90
+
91
+ # Generate auth token from username and given password.
92
+ #
93
+ # @param [String] password password to use
94
+ # @return [String] md5 digest of the username and password
95
+ def generate_auth_token( password )
96
+ self.auth_token = Digest::MD5.hexdigest( username + Digest::MD5.hexdigest(password) )
97
+ end
98
+
99
+ # Construct an HTTP GET call from params, and load the response into a LibXML Document.
100
+ #
101
+ # @param [String] method last.fm api method to call
102
+ # @param [Boolean] secure whether to sign the request with a method signature and session key
103
+ # (one exception being auth methods, which require a method signature but no session key)
104
+ # @param [Hash] params parameters to send, excluding method, api_key, api_sig, and sk
105
+ # @return [LibXML::XML::Document] xml document of the data contained in the response
106
+ # @raise [LastFMError] if the request fails
107
+ def get( method, params = {}, secure = false )
108
+ path = generate_path(method, secure, params)
109
+ logger.debug( "Last.fm HTTP GET: #{HOST}#{path}" ) if logger
110
+ response = Net::HTTP.get_response( HOST, path )
111
+ validate( LibXML::XML::Parser.string( response.body ).parse )
112
+ end
113
+
114
+ # Construct an HTTP POST call from params, and check the response status.
115
+ #
116
+ # @param [String] method last.fm api method to call
117
+ # @param [Hash] params parameters to send, excluding method, api_key, api_sig, and sk
118
+ # @return [LibXML::XML::Document] xml document of the data contained in the response
119
+ # @raise [LastFMError] if the request fails
120
+ def post( method, params )
121
+ post_uri = URI.parse("http://#{HOST}/#{API_VERSION}/")
122
+ params = construct_params( method, :secure, params )
123
+ logger.debug( "Last.fm HTTP POST: #{post_uri}, #{params.to_s}" ) if logger
124
+ response = Net::HTTP.post_form( post_uri, params )
125
+ validate( LibXML::XML::Parser.string( response.body ).parse )
126
+ end
127
+
128
+ private
129
+
130
+ # Check an XML document for status = failed, and throw a descriptive error if it's found.
131
+ #
132
+ # @param [LibXML::XML::Document] xml xml document to check for errors
133
+ # @return [LibXML::XML::Document] the xml document if no errors were found
134
+ # @raise [LastFMError] if an error is found
135
+ # @private
136
+ def validate( xml )
137
+ raise LastFMError, xml.find_first('error').content if xml.root.attributes['status'] == 'failed'
138
+ xml
139
+ end
140
+
141
+ # Normalize the parameter list by converting boolean values to 0 or 1, array values to
142
+ # comma-separated strings, and all other values to a string. Remove any nil values, and
143
+ # camel-case the parameter keys. Add method, api key, session key, and api signature
144
+ # parameters where necessary.
145
+ #
146
+ # @param [String] method last.fm api method
147
+ # @param [Boolean] secure whether to include session key and api signature in the parameters
148
+ # @param [Hash] params parameters to normalize and add to
149
+ # @return [Hash] complete, normalized parameters
150
+ # @private
151
+ def construct_params( method, secure, params )
152
+ params = params.each_with_object({}) do |(k,v), h|
153
+ v = v ? 1 : 0 if !!v == v # convert booleans into 0 or 1
154
+ v = v.compact.join(',') if v.is_a?(Array) # convert arrays into comma-separated strings
155
+ v = v.to_i if v.is_a?(Time) # convert times into integer unix timestamps
156
+ h[camel_case(k)] = v.to_s unless v.nil?
157
+ end
158
+ params['method'] = method
159
+ params['api_key'] = api_key
160
+ params['sk'] = session_key if authenticated? && secure
161
+ params['api_sig'] = generate_method_signature( params ) if secure
162
+ params
163
+ end
164
+
165
+ # Return a the camelCased version of the given string or symbol, with underscores removed,
166
+ # word capitalized, and the first letter lower case.
167
+ #
168
+ # @param [String] str the string (or symbol) to camel case
169
+ # @return [String] the camelcased version of the given string
170
+ def camel_case(key)
171
+ exceptions = { playlist_id: 'playlistID', playlist_url: 'playlistURL',
172
+ speed_multiplier: 'speed_multiplier', fingerprint_id: 'fingerprintid' }
173
+ return exceptions[key] if exceptions.include?(key)
174
+ camel = key.to_s.split('_').map{|s| s.capitalize}.join
175
+ camel[0].downcase + camel[1..-1]
176
+ end
177
+
178
+ # Generate the path for a particular method call given params.
179
+ #
180
+ # @param [String] method last.fm method to call
181
+ # @param [Boolean] secure whether to include session key and api signature in the call
182
+ # @param [optional, Hash] params parameters to include in the api call
183
+ # @return [String] path for the api call
184
+ # @private
185
+ def generate_path( method, secure, params={} )
186
+ params = construct_params( method, secure, params )
187
+ url = "/#{API_VERSION}/?method=#{params.delete('method')}"
188
+ params.inject(url) do |url, (key, value)|
189
+ url << "&#{key}=#{CGI.escape value}"
190
+ end
191
+ end
192
+
193
+ # Generate a method signature based on given parameters.
194
+ #
195
+ # @param [Hash] parameters to combine into a method signature
196
+ # @return [String] method signature based on all the parameters
197
+ # @see http://www.last.fm/api/authspec#8
198
+ # @private
199
+ def generate_method_signature( params )
200
+ str = params.keys.sort.inject('') do |str, key|
201
+ str << key << params[key]
202
+ end
203
+ Digest::MD5.hexdigest( str << api_secret )
204
+ end
205
+
206
+ end
207
+ end
data/test/test_album.rb CHANGED
@@ -1,36 +1,36 @@
1
- require 'test/unit'
2
- load 'lib/rscrobbler.rb'
3
-
4
- def check_attributes(model, attrs)
5
- attrs.each do |attr|
6
- assert_not_nil model.send(attr), "#{attr} expected to be set."
7
- end
8
- end
9
-
10
- class LastFM::AlbumTest < Test::Unit::TestCase
11
-
12
- def setup
13
- LastFM.api_key = 'b25b959554ed76058ac220b7b2e0a026'
14
- @artist = 'Cher'
15
- @album = 'Believe'
16
- end
17
-
18
- def test_search
19
- albums = LastFM::Album.search :album => @album, :limit => 1
20
- assert_equal 1, albums.size
21
- assert albums.first.is_a? LastFM::Album
22
- check_attributes albums.first, [:name, :artist, :id, :url, :images, :streamable, :mbid]
23
- end
24
-
25
- def test_get_info
26
- assert_raise LastFM::LastFMError do
27
- LastFM::Album.get_info :album => @album
28
- end
29
- album = LastFM::Album.get_info :artist => @artist, :album => @album
30
- assert_equal @artist, album.artist
31
- assert_equal @album, album.name
32
- assert album.wiki.is_a? LastFM::Wiki
33
- check_attributes album, [:artist, :id, :images, :listeners, :mbid, :name, :playcount, :release_date, :tags, :tracks, :url, :wiki]
34
- end
35
-
1
+ require 'test/unit'
2
+ load 'lib/rscrobbler.rb'
3
+
4
+ def check_attributes(model, attrs)
5
+ attrs.each do |attr|
6
+ assert_not_nil model.send(attr), "#{attr} expected to be set."
7
+ end
8
+ end
9
+
10
+ class LastFM::AlbumTest < Test::Unit::TestCase
11
+
12
+ def setup
13
+ LastFM.api_key = 'b25b959554ed76058ac220b7b2e0a026'
14
+ @artist = 'Cher'
15
+ @album = 'Believe'
16
+ end
17
+
18
+ def test_search
19
+ albums = LastFM::Album.search :album => @album, :limit => 1
20
+ assert_equal 1, albums.size
21
+ assert albums.first.is_a? LastFM::Album
22
+ check_attributes albums.first, [:name, :artist, :id, :url, :images, :streamable, :mbid]
23
+ end
24
+
25
+ def test_get_info
26
+ assert_raise LastFM::LastFMError do
27
+ LastFM::Album.get_info :album => @album
28
+ end
29
+ album = LastFM::Album.get_info :artist => @artist, :album => @album
30
+ assert_equal @artist, album.artist
31
+ assert_equal @album, album.name
32
+ assert album.wiki.is_a? LastFM::Wiki
33
+ check_attributes album, [:artist, :id, :images, :listeners, :mbid, :name, :playcount, :release_date, :tags, :tracks, :url, :wiki]
34
+ end
35
+
36
36
  end