simplespotify 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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.md +17 -0
- data/Rakefile +3 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/simplespotify/actions/albums.rb +30 -0
- data/lib/simplespotify/actions/artists.rb +47 -0
- data/lib/simplespotify/actions/browse.rb +74 -0
- data/lib/simplespotify/actions/playlists.rb +187 -0
- data/lib/simplespotify/actions/tracks.rb +67 -0
- data/lib/simplespotify/actions/users.rb +68 -0
- data/lib/simplespotify/authorization.rb +91 -0
- data/lib/simplespotify/client.rb +76 -0
- data/lib/simplespotify/constants.rb +14 -0
- data/lib/simplespotify/errors.rb +56 -0
- data/lib/simplespotify/models/album.rb +27 -0
- data/lib/simplespotify/models/artist.rb +42 -0
- data/lib/simplespotify/models/category.rb +19 -0
- data/lib/simplespotify/models/collection.rb +52 -0
- data/lib/simplespotify/models/image.rb +16 -0
- data/lib/simplespotify/models/playlist.rb +94 -0
- data/lib/simplespotify/models/track.rb +31 -0
- data/lib/simplespotify/models/user.rb +91 -0
- data/lib/simplespotify/request.rb +54 -0
- data/lib/simplespotify/resource/class_methods.rb +48 -0
- data/lib/simplespotify/resource/instance_methods.rb +102 -0
- data/lib/simplespotify/resource/resource.rb +14 -0
- data/lib/simplespotify/response.rb +22 -0
- data/lib/simplespotify/version.rb +3 -0
- data/lib/simplespotify.rb +72 -0
- data/readme.md +71 -0
- data/simplespotify.gemspec +26 -0
- metadata +122 -0
@@ -0,0 +1,76 @@
|
|
1
|
+
module SimpleSpotify
|
2
|
+
class Client
|
3
|
+
|
4
|
+
attr_accessor :id, :secret, :session, :market
|
5
|
+
|
6
|
+
[:Albums, :Tracks, :Artists, :Users, :Browse, :Playlists].each do |action|
|
7
|
+
require "simplespotify/actions/#{action.downcase}"
|
8
|
+
include Actions.const_get(action)
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize client_id, client_secret, default: true
|
12
|
+
@id = client_id
|
13
|
+
@secret = client_secret
|
14
|
+
@session = nil
|
15
|
+
@market = nil
|
16
|
+
SimpleSpotify.default_client = self if default
|
17
|
+
end
|
18
|
+
|
19
|
+
[:get, :post, :put, :delete].each do |verb|
|
20
|
+
define_method(verb) do |msg, data={}|
|
21
|
+
unless msg.is_a? SimpleSpotify::Request
|
22
|
+
msg = Request.new(msg, {method: verb, data: data})
|
23
|
+
end
|
24
|
+
SimpleSpotify.dispatch msg, session: session
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
def search type, term, market: nil, limit: 20, offset: 0, filters: {}
|
30
|
+
type = type.to_s.gsub(/s$/, '').to_sym
|
31
|
+
types = [:album, :artist, :playlist, :track]
|
32
|
+
raise "Can't search for <#{type}> only: #{types.join(',')}" unless types.include?(type)
|
33
|
+
|
34
|
+
params = {q: term}
|
35
|
+
params[:market] = market || @market
|
36
|
+
params[:limit] = limit if limit
|
37
|
+
params[:offset] = offset if offset
|
38
|
+
|
39
|
+
filters = filters.map { |filter, value|
|
40
|
+
value = value.join(',') if value.is_a?(Array)
|
41
|
+
value = [value.min, value.max].map(&:to_s).join('-') if value.is_a?(Range)
|
42
|
+
%{#{filter}:"#{value}"}
|
43
|
+
}
|
44
|
+
|
45
|
+
params[:q] = ([term]+filters).join(' ')
|
46
|
+
params[:type] = type
|
47
|
+
|
48
|
+
params = params.reject {|k,v| v.nil?}.to_h
|
49
|
+
response = get "search", params
|
50
|
+
|
51
|
+
Model::Collection.of(type, response.body["#{type}s".to_sym])
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
def to_h
|
56
|
+
{
|
57
|
+
client_secret: secret,
|
58
|
+
client_id: id
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def id_for object
|
66
|
+
return object.id if object.is_a? Resource
|
67
|
+
object
|
68
|
+
end
|
69
|
+
|
70
|
+
def options_with_market user_market, options={}
|
71
|
+
options[:market] = user_market || @market
|
72
|
+
options.reject {|k,v| v.nil? || v=='' }.to_h
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module SimpleSpotify
|
2
|
+
#--------------
|
3
|
+
# Constants
|
4
|
+
#--------------
|
5
|
+
|
6
|
+
AUTHORIZATION_URL = 'https://accounts.spotify.com/authorize'
|
7
|
+
TOKEN_URL = 'https://accounts.spotify.com/api/token'
|
8
|
+
|
9
|
+
ENDPOINT = 'https://api.spotify.com/v1/'
|
10
|
+
|
11
|
+
# Upper bound when trying to refresh the authentication token
|
12
|
+
MAX_RETRIES = 2
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module SimpleSpotify
|
2
|
+
module Error
|
3
|
+
|
4
|
+
def self.for status_code
|
5
|
+
case status_code
|
6
|
+
when 401 then Unauthorized
|
7
|
+
when 404 then NotFound
|
8
|
+
when (500..599) then API
|
9
|
+
else
|
10
|
+
DefaultError
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
class AuthorizationError < StandardError
|
16
|
+
attr_reader :message
|
17
|
+
def initialize body
|
18
|
+
@message = body[:error_description]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
class DefaultError < StandardError
|
24
|
+
attr_reader :code, :body, :message, :request
|
25
|
+
def initialize code, body, request
|
26
|
+
@code = code
|
27
|
+
@body = body
|
28
|
+
@request = request
|
29
|
+
@message = body[:error][:message] unless body.nil?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class BadResponse < StandardError
|
34
|
+
attr_reader :code, :message, :request, :body
|
35
|
+
def initialize code, body, request
|
36
|
+
@code = code
|
37
|
+
@message = 'Unreadable JSON data returned from SimpleSpotify API'
|
38
|
+
@request = request
|
39
|
+
@body = body
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class API < DefaultError
|
44
|
+
end
|
45
|
+
|
46
|
+
class NotFound < DefaultError
|
47
|
+
def initialize code, body, request
|
48
|
+
@message = "#{request.full_url} not found"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class Unauthorized < DefaultError
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module SimpleSpotify
|
2
|
+
module Model
|
3
|
+
class Album
|
4
|
+
include Resource
|
5
|
+
|
6
|
+
prop :type, from: :album_type
|
7
|
+
|
8
|
+
many :artists
|
9
|
+
prop :markets
|
10
|
+
prop :copyrights
|
11
|
+
|
12
|
+
merge :external_ids
|
13
|
+
|
14
|
+
prop :genres, default: []
|
15
|
+
many :images
|
16
|
+
|
17
|
+
prop :name
|
18
|
+
prop :popularity
|
19
|
+
prop :release_date
|
20
|
+
prop :release_date_precision
|
21
|
+
|
22
|
+
many :tracks, paginated: true
|
23
|
+
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module SimpleSpotify
|
2
|
+
module Model
|
3
|
+
class Artist
|
4
|
+
include Resource
|
5
|
+
|
6
|
+
merge :external_ids
|
7
|
+
|
8
|
+
prop :genres
|
9
|
+
many :images
|
10
|
+
prop :name
|
11
|
+
prop :popularity
|
12
|
+
|
13
|
+
prop :followers
|
14
|
+
|
15
|
+
|
16
|
+
def related_artists! client=nil
|
17
|
+
client ||= SimpleSpotify.default_client
|
18
|
+
@related_artists = client.related_artists(id)
|
19
|
+
end
|
20
|
+
|
21
|
+
def related_artists client=nil
|
22
|
+
@related_artists || related_artists!(client)
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def top_tracks! client=nil
|
27
|
+
client ||= SimpleSpotify.default_client
|
28
|
+
@top_tracks = client.top_tracks(id)
|
29
|
+
end
|
30
|
+
|
31
|
+
def top_tracks client=nil
|
32
|
+
@top_tracks || top_tracks!(client)
|
33
|
+
end
|
34
|
+
|
35
|
+
def albums! client=nil
|
36
|
+
client ||= SimpleSpotify.default_client
|
37
|
+
@albums = client.artist_albums(id)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module SimpleSpotify
|
2
|
+
module Models
|
3
|
+
class Category
|
4
|
+
|
5
|
+
many :icons, kind: :image
|
6
|
+
prop :name
|
7
|
+
|
8
|
+
def playlists client=nil, country: nil, limit: 20, offset: 0
|
9
|
+
@playlists || playlists!(client, country: country, limit: limit, offset: offset)
|
10
|
+
end
|
11
|
+
|
12
|
+
def playlists! client=nil, country: nil, limit: 20, offset: 0
|
13
|
+
client ||= SimpleSpotify.default_client
|
14
|
+
@playlists = client.category_playlists(id, country: country, limit: limit, offset: offset)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module SimpleSpotify
|
2
|
+
module Model
|
3
|
+
|
4
|
+
class Collection < Array
|
5
|
+
|
6
|
+
attr_accessor :total
|
7
|
+
|
8
|
+
def self.of type, data
|
9
|
+
if type.is_a? Class
|
10
|
+
model = type;
|
11
|
+
prop = type.to_s.split('::').last.downcase+'s'
|
12
|
+
else
|
13
|
+
model_name = type.to_s.gsub(/s$/, '')
|
14
|
+
model = Model.const_get(model_name.capitalize)
|
15
|
+
prop = model_name+'s'
|
16
|
+
end
|
17
|
+
|
18
|
+
prop = prop.to_sym
|
19
|
+
prop = :items unless data.has_key?(prop)
|
20
|
+
|
21
|
+
data[prop].map! {|item| model.new(item) }
|
22
|
+
self.new(data)
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
def initialize data, property=:items
|
27
|
+
values = data[property]
|
28
|
+
super values
|
29
|
+
@total = data[:total] || self.count
|
30
|
+
@next = data[:next]
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def total
|
35
|
+
@total
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
def more?
|
40
|
+
!@next.nil?
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def more
|
45
|
+
return [] unless more?
|
46
|
+
req = Request.new({endpoint: @next, private: false})
|
47
|
+
SimpleSpotify.dispatch(req)
|
48
|
+
end
|
49
|
+
end #/class
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module SimpleSpotify
|
2
|
+
module Model
|
3
|
+
class Playlist
|
4
|
+
include Resource
|
5
|
+
|
6
|
+
prop :name
|
7
|
+
prop :description
|
8
|
+
|
9
|
+
prop :collaborative
|
10
|
+
prop :public
|
11
|
+
|
12
|
+
many :images
|
13
|
+
one :owner, kind: :user
|
14
|
+
|
15
|
+
prop :snapshot_id
|
16
|
+
|
17
|
+
many :tracks, kind: :playlist_track, paginated: true
|
18
|
+
|
19
|
+
def public?
|
20
|
+
@public == true
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def change_details! client=nil
|
25
|
+
client ||= SimpleSpotify.default_client
|
26
|
+
client.change_details(self, name: name, public: @public)
|
27
|
+
end
|
28
|
+
|
29
|
+
#--------------
|
30
|
+
# Tracks
|
31
|
+
#--------------
|
32
|
+
def tracks! client=nil, options={}
|
33
|
+
client ||= SimpleSpotify.default_client
|
34
|
+
client.playlist_tracks(self, options)
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_tracks tracks, position=nil, client=nil
|
38
|
+
client ||= SimpleSpotify.default_client
|
39
|
+
client.playlist_tracks_add(self, tracks: tracks, position: position)
|
40
|
+
end
|
41
|
+
|
42
|
+
def remove_tracks client=nil, tracks: nil, positions: nil, snapshot_id: nil
|
43
|
+
client ||= SimpleSpotify.default_client
|
44
|
+
snapshot_id ||= @snapshot_id unless positions.nil?
|
45
|
+
client.playlist_tracks_remove(self, tracks: tracks, positions: positions, snapshot_id: snapshot_id)
|
46
|
+
end
|
47
|
+
|
48
|
+
def reorder_tracks client=nil, payload
|
49
|
+
client ||= SimpleSpotify.default_client
|
50
|
+
client.playlist_tracks_reorder(self, payload)
|
51
|
+
end
|
52
|
+
|
53
|
+
def replace_tracks client=nil, tracks: []
|
54
|
+
client ||= SimpleSpotify.default_client
|
55
|
+
client.playlist_tracks_reorder(self, tracks: tracks)
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
#--------------
|
60
|
+
# Follows
|
61
|
+
#--------------
|
62
|
+
def follow client=nil, public: true
|
63
|
+
client ||= SimpleSpotify.default_client
|
64
|
+
client.playlist_follow(self, public: public)
|
65
|
+
end
|
66
|
+
|
67
|
+
def unfollow
|
68
|
+
client ||= SimpleSpotify.default_client
|
69
|
+
client.playlist_unfollow(self)
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
#--------------
|
74
|
+
# Create
|
75
|
+
#--------------
|
76
|
+
def self.create user, name, client=nil, public:true
|
77
|
+
client ||= SimpleSpotify.default_client
|
78
|
+
client.playlist_create(user, name, public: public)
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
class PlaylistTrack
|
85
|
+
include Resource
|
86
|
+
|
87
|
+
prop :added_at
|
88
|
+
prop :added_by
|
89
|
+
prop :is_local
|
90
|
+
one :track
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module SimpleSpotify
|
2
|
+
module Model
|
3
|
+
class Track
|
4
|
+
include Resource
|
5
|
+
|
6
|
+
many :albums
|
7
|
+
many :artists
|
8
|
+
|
9
|
+
prop :markets, from: :available_markets
|
10
|
+
prop :disc_number
|
11
|
+
prop :duration_ms
|
12
|
+
prop :explicit
|
13
|
+
merge :external_ids
|
14
|
+
prop :playable, from: :is_playable
|
15
|
+
|
16
|
+
prop :name
|
17
|
+
prop :popularity
|
18
|
+
prop :preview_url
|
19
|
+
prop :track_number
|
20
|
+
|
21
|
+
def duration
|
22
|
+
Time.at(@duration_ms/1000).utc.strftime("%H:%M:%S")
|
23
|
+
end
|
24
|
+
|
25
|
+
def playable?
|
26
|
+
playable == true
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module SimpleSpotify
|
2
|
+
module Model
|
3
|
+
class User
|
4
|
+
include Resource
|
5
|
+
|
6
|
+
prop :birthdate
|
7
|
+
prop :country
|
8
|
+
prop :display_name
|
9
|
+
prop :email
|
10
|
+
|
11
|
+
prop :external_urls
|
12
|
+
prop :followers
|
13
|
+
|
14
|
+
many :images
|
15
|
+
prop :product
|
16
|
+
|
17
|
+
def name
|
18
|
+
display_name
|
19
|
+
end
|
20
|
+
|
21
|
+
def premium?
|
22
|
+
@product == 'premium'
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
#--------------
|
27
|
+
# Saved tracks
|
28
|
+
#--------------
|
29
|
+
def save_tracks ids, client=nil
|
30
|
+
client ||= SimpleSpotify.default_client
|
31
|
+
client.save_tracks(ids)
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def remove_tracks ids, client=nil
|
36
|
+
client ||= SimpleSpotify.default_client
|
37
|
+
client.remove_tracks(ids)
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
def saved_tracks_include? ids, client=nil
|
42
|
+
client ||= SimpleSpotify.default_client
|
43
|
+
client.saved_tracks_include?(ids)
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
#--------------
|
48
|
+
# Follows
|
49
|
+
#--------------
|
50
|
+
def follow type, ids, client=nil
|
51
|
+
client ||= SimpleSpotify.default_client
|
52
|
+
client.user_follow(type, ids)
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def unfollow type, ids, client=nil
|
57
|
+
client ||= SimpleSpotify.default_client
|
58
|
+
client.user_unfollow(type, ids)
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
def following? type, ids, client=nil
|
63
|
+
client ||= SimpleSpotify.default_client
|
64
|
+
client.user_following(type, ids)
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
#--------------
|
69
|
+
# Playlists
|
70
|
+
#--------------
|
71
|
+
|
72
|
+
def playlists! client=nil, fields: {}, market: nil, limit: 20, offset: 0
|
73
|
+
client ||= SimpleSpotify.default_client
|
74
|
+
client.playlists(self, fields: fields, market: market, limit: limit, offset: offset)
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
def follow_playlist owner, playlist=nil, client=nil, public: true
|
79
|
+
client ||= SimpleSpotify.default_client
|
80
|
+
client.playlist_follow(owner, playlist, public: public)
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
def follow_playlist owner, playlist=nil, client=nil
|
85
|
+
client ||= SimpleSpotify.default_client
|
86
|
+
client.playlist_unfollow(owner, playlist)
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module SimpleSpotify
|
2
|
+
class Request
|
3
|
+
|
4
|
+
attr_accessor :endpoint, :method, :json, :data, :tries
|
5
|
+
|
6
|
+
def initialize endpoint, opts = {}
|
7
|
+
endpoint = endpoint.join('/') if endpoint.respond_to? :join
|
8
|
+
|
9
|
+
@endpoint = endpoint
|
10
|
+
@headers = opts[:headers] || {}
|
11
|
+
@method = opts[:method] || :get
|
12
|
+
@json = opts[:json] || (@method != :get)
|
13
|
+
@tries = 0
|
14
|
+
@data = opts[:data]
|
15
|
+
@query = opts[:query]
|
16
|
+
@private = opts[:private] || true
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
def private?
|
21
|
+
@private;
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def method
|
26
|
+
@method.to_sym
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def options
|
31
|
+
return {} unless data
|
32
|
+
if method == :get
|
33
|
+
{query: data}
|
34
|
+
else
|
35
|
+
opts = {body: data.to_json}
|
36
|
+
opts[:query] = @query if @query
|
37
|
+
opts
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def headers
|
43
|
+
h = @headers
|
44
|
+
h['Content-type'] = 'application/json' if json
|
45
|
+
h
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def full_url
|
50
|
+
SimpleSpotify::ENDPOINT + endpoint.to_s.gsub(SimpleSpotify::ENDPOINT, '')
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Resource
|
2
|
+
module ClassMethods
|
3
|
+
|
4
|
+
def self.extended(base)
|
5
|
+
super base
|
6
|
+
end
|
7
|
+
|
8
|
+
def prop name, o={}
|
9
|
+
p = (o[:from] || name)
|
10
|
+
p = {type: :real, from: p, default: o[:default]} if o[:default]
|
11
|
+
@_template[name] = p
|
12
|
+
attr_accessor name.to_sym
|
13
|
+
end
|
14
|
+
|
15
|
+
def many name, o={}
|
16
|
+
attr_accessor name.to_sym
|
17
|
+
kind = o[:kind] || name
|
18
|
+
@_template[name] = {
|
19
|
+
type: :resource_collection,
|
20
|
+
kind: _constant(kind),
|
21
|
+
from: name,
|
22
|
+
paginated: o[:paginated] || false
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def one name, o={}
|
27
|
+
attr_accessor name.to_sym
|
28
|
+
kind = o[:kind] || name
|
29
|
+
@_template[name] = {
|
30
|
+
type: :resource,
|
31
|
+
kind: _constant(kind),
|
32
|
+
from: (o[:from] || name)
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def merge name
|
37
|
+
@_template[name] = {type: :virtual, from: name}
|
38
|
+
end
|
39
|
+
|
40
|
+
def _template
|
41
|
+
@_template
|
42
|
+
end
|
43
|
+
|
44
|
+
def _constant name
|
45
|
+
"#{name}".gsub(/s$/, '').split('_').map(&:capitalize).join.to_sym
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|