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.
@@ -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,16 @@
1
+ module SimpleSpotify
2
+ module Model
3
+ class Image
4
+ include Resource
5
+
6
+ prop :height
7
+ prop :width
8
+ prop :url
9
+
10
+ def to_s
11
+ url
12
+ end
13
+
14
+ end
15
+ end
16
+ 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