spot 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/.rspec +5 -0
- data/Gemfile +2 -0
- data/README.markdown +255 -0
- data/Rakefile +2 -0
- data/lib/spot.rb +214 -0
- data/lib/spot/album.rb +19 -0
- data/lib/spot/artist.rb +12 -0
- data/lib/spot/base.rb +58 -0
- data/lib/spot/exclude.yml +15 -0
- data/lib/spot/song.rb +32 -0
- data/spec/album_spec.rb +30 -0
- data/spec/artist_spec.rb +20 -0
- data/spec/base_spec.rb +35 -0
- data/spec/fixtures/album.json +1 -0
- data/spec/fixtures/artist.json +1 -0
- data/spec/fixtures/exclude.tribute.json +39 -0
- data/spec/fixtures/track.json +1 -0
- data/spec/song_spec.rb +58 -0
- data/spec/spec_helper.rb +35 -0
- data/spec/spotify_spec.rb +433 -0
- data/spot.gemspec +31 -0
- metadata +163 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,255 @@
|
|
1
|
+
# Spot
|
2
|
+
|
3
|
+
A Ruby implementation of the [Spotify Meta API](http://developer.spotify.com/en/metadata-api/overview/).
|
4
|
+
|
5
|
+
This gem is used internally at the [Radiofy](http://radiofy.se) project.
|
6
|
+
|
7
|
+
Follow me on [Twitter](http://twitter.com/linusoleander) for more info and updates.
|
8
|
+
|
9
|
+
## How to use
|
10
|
+
|
11
|
+
### Find a song
|
12
|
+
|
13
|
+
The `Spot.find_song` method returns the first hit.
|
14
|
+
|
15
|
+
```` ruby
|
16
|
+
Spot.find_song("Like Glue")
|
17
|
+
````
|
18
|
+
|
19
|
+
### Find all songs
|
20
|
+
|
21
|
+
The `find_all_songs` method returns a list of `Song` objects.
|
22
|
+
|
23
|
+
```` ruby
|
24
|
+
Spot.find_all_songs("Like Glue")
|
25
|
+
````
|
26
|
+
|
27
|
+
### Find an artist
|
28
|
+
|
29
|
+
The `Spot.find_artist` method returns the first hit.
|
30
|
+
|
31
|
+
```` ruby
|
32
|
+
Spot.find_artist("Madonna")
|
33
|
+
````
|
34
|
+
|
35
|
+
### Find all artists
|
36
|
+
|
37
|
+
The `find_all_artists` method returns a list of `Artist` objects.
|
38
|
+
|
39
|
+
```` ruby
|
40
|
+
Spot.find_all_artists("Madonna")
|
41
|
+
````
|
42
|
+
|
43
|
+
### Find an album
|
44
|
+
|
45
|
+
The `Spot.find_album` method returns the first hit.
|
46
|
+
|
47
|
+
```` ruby
|
48
|
+
Spot.find_album("Old Skool Of Rock")
|
49
|
+
````
|
50
|
+
|
51
|
+
### Find all albums
|
52
|
+
|
53
|
+
The `find_all_albums` method returns a list of `Album` objects.
|
54
|
+
|
55
|
+
```` ruby
|
56
|
+
Spot.find_all_albums("Old Skool Of Rock")
|
57
|
+
````
|
58
|
+
|
59
|
+
### Find best match
|
60
|
+
|
61
|
+
The `prime` method makes it possible to fetch the best matching result based on the ingoing argument.
|
62
|
+
|
63
|
+
Here is what is being returned *without* the `prime` method.
|
64
|
+
|
65
|
+
>> Spot.find_song("sweet home").result
|
66
|
+
=> Home Sweet Home - Mötley Crüe
|
67
|
+
|
68
|
+
Here is what is being returned *with* the `prime` method.
|
69
|
+
|
70
|
+
>> Spot.prime.find_song("sweet home").result
|
71
|
+
=> Sweet Home Alabama - Lynyrd Skynyrd
|
72
|
+
|
73
|
+
The `prime` method will reject data (songs, artists and albums) that contains any of the [these words](https://github.com/oleander/Spot/blob/master/lib/spot/exclude.yml).
|
74
|
+
|
75
|
+
Here is the short version.
|
76
|
+
|
77
|
+
- tribute
|
78
|
+
- cover
|
79
|
+
- remix
|
80
|
+
- live
|
81
|
+
- club mix
|
82
|
+
- karaoke
|
83
|
+
- remaster
|
84
|
+
- club version
|
85
|
+
- demo
|
86
|
+
- made famous by
|
87
|
+
- remixes
|
88
|
+
- instrumental
|
89
|
+
- ringtone
|
90
|
+
|
91
|
+
Take a look at the [source code](https://github.com/oleander/Spot/blob/master/lib/spot.rb#L94) for more information.
|
92
|
+
|
93
|
+
### Specify a territory
|
94
|
+
|
95
|
+
All songs in Spotify isn't available everywhere.
|
96
|
+
Therefore it might be usefull to specify a location, also know as a *territory*.
|
97
|
+
|
98
|
+
If you for example want to find all songs available in Sweden, then you might do something like this.
|
99
|
+
|
100
|
+
```` ruby
|
101
|
+
Spot.territory("SE").find_song("Sweet Home Alabama")
|
102
|
+
````
|
103
|
+
|
104
|
+
You can find the complete territory list [here](http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2).
|
105
|
+
|
106
|
+
### Filter ingoing arguments
|
107
|
+
|
108
|
+
Sometimes it may be useful to filer ingoing params.
|
109
|
+
You can filter the ingoing string by using the `strip` method.
|
110
|
+
|
111
|
+
```` ruby
|
112
|
+
Spot.strip.find_song("3. Who's That Chick ? feat.Rihanna [Singel Version] - (Single)")
|
113
|
+
````
|
114
|
+
|
115
|
+
This is the string that is being passed to Spot.
|
116
|
+
|
117
|
+
"who's that chick ?"
|
118
|
+
|
119
|
+
Take a look at the [source code](https://github.com/oleander/Spot/blob/master/lib/spot.rb#L136) if you want to know what regexp is being used.
|
120
|
+
|
121
|
+
### Specify a page
|
122
|
+
|
123
|
+
You can easily select any page you want by defining the `page` method.
|
124
|
+
|
125
|
+
```` ruby
|
126
|
+
Spot.page(11).find_song("sweet home")
|
127
|
+
````
|
128
|
+
|
129
|
+
The default page is of course `1`. :)
|
130
|
+
|
131
|
+
### Combine methods
|
132
|
+
|
133
|
+
You can easily combine method like this.
|
134
|
+
|
135
|
+
```` ruby
|
136
|
+
Spot.page(11).territory("SE").prime.strip.find_song("sweet home")
|
137
|
+
````
|
138
|
+
|
139
|
+
## Data to work with
|
140
|
+
|
141
|
+
As soon as the `result` or `results` method is applied to the query a request to Spotify is made.
|
142
|
+
|
143
|
+
Here is an example using the `result` method.
|
144
|
+
|
145
|
+
>> song = Spot.find_song("sweet home").result
|
146
|
+
|
147
|
+
>> puts song.title
|
148
|
+
=> Home Sweet Home
|
149
|
+
|
150
|
+
>> puts song.class
|
151
|
+
=> SpotContainer::Song
|
152
|
+
|
153
|
+
Here is an example using the `results` method.
|
154
|
+
|
155
|
+
>> songs = Spot.find_all_songs("sweet home").results
|
156
|
+
>> puts songs.count
|
157
|
+
=> 100
|
158
|
+
|
159
|
+
### Base
|
160
|
+
|
161
|
+
All classes, `Song`, `Artist` and `Album` share these methods.
|
162
|
+
|
163
|
+
- **popularity** (*Float*) Popularity acording to Spotify. From `0.0` to `1.0`.
|
164
|
+
- **href** (*String*) Url for the specific object.
|
165
|
+
Default is a spotify url on this format: `spotify:track:5DhDGwNXRPHsMApbtVKvFb`.
|
166
|
+
`http` may be passed as a string, which will return an Spotify HTTP Url on this format: `http://open.spotify.com/track/5DhDGwNXRPHsMApbtVKvFb`.
|
167
|
+
- **available?** (*Boolean*) Takes one argument, a territory. Returns true if the object is accessible in the given region.
|
168
|
+
Read more about it in the *Specify a territory* section above.
|
169
|
+
- **to_s** (*String*) A string representation of the object.
|
170
|
+
- **valid?** (*Boolean*) Returns true if the object is valid, a.k.a is accessible in the given territory.
|
171
|
+
If no territory is given, this will be true.
|
172
|
+
- **name** (*String*) Name of the `Song`, `Artist` or `Album`. This method will return the same thing as `Song#title`.
|
173
|
+
|
174
|
+
### Song
|
175
|
+
|
176
|
+
Methods available for the `Song` class.
|
177
|
+
|
178
|
+
- **length** (*Fixnum*) Length in seconds.
|
179
|
+
- **title** (*String*) Song title.
|
180
|
+
- **to_s** (*String*) String representation of the object in this format: *song - artist*.
|
181
|
+
- **artist** (*Artist*) The artist.
|
182
|
+
- **album** (*Album*) The album.
|
183
|
+
|
184
|
+
### Artist
|
185
|
+
|
186
|
+
Methods available for the `Artist` class.
|
187
|
+
|
188
|
+
- **name** (*String*) Name of the artist.
|
189
|
+
- **to_s** (*String*) Same as above.
|
190
|
+
|
191
|
+
### Album
|
192
|
+
|
193
|
+
Methods available for the `Album` class.
|
194
|
+
|
195
|
+
- **artist** (*Artist*) The artist.
|
196
|
+
|
197
|
+
### Spot
|
198
|
+
|
199
|
+
This one is easier to explain in plain code.
|
200
|
+
|
201
|
+
```` ruby
|
202
|
+
spot = Spot.find_song("kaizers orchestra")
|
203
|
+
|
204
|
+
puts spot.num_results # => 188
|
205
|
+
puts spot.limit # => 100
|
206
|
+
puts spot.offset # => 0
|
207
|
+
puts spot.query # => "kaizers orchestra"
|
208
|
+
````
|
209
|
+
|
210
|
+
- **num_results** (*Fixnum*) The amount of hits.
|
211
|
+
- **limit** (*Fixnum*) The amount of results on each page.
|
212
|
+
- **offset** (*Fixnum*) Unknown.
|
213
|
+
- **query** (*String*) The search param that was passed to Spotify.
|
214
|
+
|
215
|
+
## Request limit!
|
216
|
+
|
217
|
+
**Be aware**: Spotify has an request limit set for 10 requests per second.
|
218
|
+
Which means that you can't just use it like this.
|
219
|
+
|
220
|
+
```` ruby
|
221
|
+
["song1", "song2" ... ].each do |song|
|
222
|
+
Spot.find_song(song)
|
223
|
+
# Do something with the data.
|
224
|
+
end
|
225
|
+
````
|
226
|
+
|
227
|
+
Instead use something like [Wire](https://github.com/oleander/Wire) to limit the amount of requests per seconds.
|
228
|
+
|
229
|
+
```` ruby
|
230
|
+
require "rubygems"
|
231
|
+
require "wire"
|
232
|
+
require "spot"
|
233
|
+
|
234
|
+
wires = []
|
235
|
+
["song1", "song2" ... ].each do |s|
|
236
|
+
wires << Wire.new(max: 10, wait: 1, vars: [s]) do |song|
|
237
|
+
Spot.find_song(song)
|
238
|
+
# Do something with the data.
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
wires.map(&:join)
|
243
|
+
````
|
244
|
+
|
245
|
+
## How do install
|
246
|
+
|
247
|
+
[sudo] gem install spot
|
248
|
+
|
249
|
+
## Requirements
|
250
|
+
|
251
|
+
*Spot* is tested in *OS X 10.6.7* using Ruby *1.8.7*, *1.9.2*.
|
252
|
+
|
253
|
+
## License
|
254
|
+
|
255
|
+
*Spot* is released under the *MIT license*.
|
data/Rakefile
ADDED
data/lib/spot.rb
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require "spot/song"
|
3
|
+
require "spot/artist"
|
4
|
+
require "spot/album"
|
5
|
+
require "json/pure"
|
6
|
+
require "rest-client"
|
7
|
+
require "uri"
|
8
|
+
require "levenshteinish"
|
9
|
+
require "rchardet19"
|
10
|
+
require "iconv"
|
11
|
+
require "yaml"
|
12
|
+
|
13
|
+
class Spot
|
14
|
+
def initialize
|
15
|
+
@methods = {
|
16
|
+
:artists => {
|
17
|
+
:selector => :artists,
|
18
|
+
:class => SpotContainer::Artist,
|
19
|
+
:url => generate_url("artist")
|
20
|
+
},
|
21
|
+
:songs => {
|
22
|
+
:selector => :tracks,
|
23
|
+
:class => SpotContainer::Song,
|
24
|
+
:url => generate_url("track")
|
25
|
+
},
|
26
|
+
:albums => {
|
27
|
+
:selector => :albums,
|
28
|
+
:class => SpotContainer::Album,
|
29
|
+
:url => generate_url("album")
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
@cache = {}
|
34
|
+
|
35
|
+
@exclude = YAML.load(File.read("#{File.dirname(__FILE__)}/spot/exclude.yml"))
|
36
|
+
|
37
|
+
@config = {
|
38
|
+
:exclude => 2,
|
39
|
+
:popularity => 7,
|
40
|
+
:limit => 0.7,
|
41
|
+
:offset => 10
|
42
|
+
}
|
43
|
+
|
44
|
+
@options = {}
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.method_missing(method, *args, &blk)
|
48
|
+
Spot.new.send(method, *args, &blk)
|
49
|
+
end
|
50
|
+
|
51
|
+
def method_missing(method, *args, &blk)
|
52
|
+
if method.to_s =~ /^find(_all)?_([a-z]+)$/i
|
53
|
+
find($2, !!$1, args.first)
|
54
|
+
elsif scrape and content["info"].keys.include?(method.to_s)
|
55
|
+
content["info"][method.to_s]
|
56
|
+
else
|
57
|
+
super(method, *args, &blk)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def page(value)
|
62
|
+
tap { @page = value }
|
63
|
+
end
|
64
|
+
|
65
|
+
def prime
|
66
|
+
tap { @prime = true }
|
67
|
+
end
|
68
|
+
|
69
|
+
def prefix(value)
|
70
|
+
tap { @prefix = value }
|
71
|
+
end
|
72
|
+
|
73
|
+
def find(type, all, s)
|
74
|
+
tap {
|
75
|
+
@search = s
|
76
|
+
@type = all ? type.to_sym : "#{type}s".to_sym
|
77
|
+
raise NoMethodError.new(@type) unless @methods.keys.include?(@type)
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
def results
|
82
|
+
@_results ||= scrape
|
83
|
+
end
|
84
|
+
|
85
|
+
def strip
|
86
|
+
tap { @strip = true }
|
87
|
+
end
|
88
|
+
|
89
|
+
def territory(value)
|
90
|
+
tap { @options.merge!(:territory => value) }
|
91
|
+
end
|
92
|
+
|
93
|
+
def result
|
94
|
+
@prime ? results.map do |r|
|
95
|
+
|
96
|
+
song, artist = type_of(r)
|
97
|
+
|
98
|
+
match = "#{song} #{artist}".split(" ")
|
99
|
+
raw = clean!(search).split(" ")
|
100
|
+
|
101
|
+
if raw.length < match.length
|
102
|
+
diff = match - raw
|
103
|
+
res = diff.length.to_f/match.length
|
104
|
+
else
|
105
|
+
diff = raw - match
|
106
|
+
res = diff.length.to_f/raw.length
|
107
|
+
end
|
108
|
+
|
109
|
+
if diff.length > 1 and not match.map{ |m| diff.include?(m) }.all?
|
110
|
+
res =+ diff.map do |value|
|
111
|
+
match.map do |m|
|
112
|
+
Levenshtein.distance(value, m)
|
113
|
+
end.inject(:+)
|
114
|
+
end.inject(:+) / @config[:offset]
|
115
|
+
end
|
116
|
+
|
117
|
+
[res - r.popularity/@config[:popularity], r]
|
118
|
+
end.reject do |distance, value|
|
119
|
+
exclude?(value.to_s)
|
120
|
+
end.sort_by do |distance, _|
|
121
|
+
distance
|
122
|
+
end.map(&:last).first : results.first
|
123
|
+
end
|
124
|
+
|
125
|
+
def type_of(r)
|
126
|
+
if @type == :songs
|
127
|
+
return r.name.to_s, r.artist.name.to_s
|
128
|
+
elsif @type == :artists
|
129
|
+
return r.song.title.to_s, r.name.to_s
|
130
|
+
else
|
131
|
+
return "", r.artist.to_s
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def clean!(string)
|
136
|
+
string.strip!
|
137
|
+
|
138
|
+
# Song - A + B + C => Song - A
|
139
|
+
# Song - A abc/def => Song - A abc
|
140
|
+
# Song - A & abc def => Song - A
|
141
|
+
# Song - A "abc def" => Song - A
|
142
|
+
# Song - A [B + C] => Song - A
|
143
|
+
# Song A B.mp3 => Song A B
|
144
|
+
# Song a.b.c.d.e => Song a b c d e
|
145
|
+
# 10. Song => Song
|
146
|
+
[/\.[a-z0-9]{2,3}$/, /\[[^\]]*\]/,/".*"/, /'.*'/, /[&|\/|\+][^\z]*/, /^(\d+.*?[^a-z]+?)/i].each do |reg|
|
147
|
+
string = string.gsub(reg, '').strip
|
148
|
+
end
|
149
|
+
|
150
|
+
[/\(.+?\)/m, /feat(.*?)\s*[^\s]+/i, /[-]+/, /[\s]+/m, /\./, /\_/].each do |reg|
|
151
|
+
string = string.gsub(reg, ' ').strip
|
152
|
+
end
|
153
|
+
|
154
|
+
{"ä" => "a", "å" => "a", "ö" => "o"}.each do |from, to|
|
155
|
+
string.gsub!(/#{from}/i, to)
|
156
|
+
end
|
157
|
+
|
158
|
+
string.gsub(/\A\s|\s\z/, '').gsub(/\s+/, ' ').strip.downcase
|
159
|
+
rescue Encoding::CompatibilityError
|
160
|
+
return string
|
161
|
+
end
|
162
|
+
|
163
|
+
def exclude?(compare)
|
164
|
+
@exclude.map { |value| !! compare.match(/#{value}/i) }.any?
|
165
|
+
end
|
166
|
+
|
167
|
+
private
|
168
|
+
def url
|
169
|
+
@url ||= @methods[@type][:url].
|
170
|
+
gsub(/<SEARCH>/, URI.escape(search)).
|
171
|
+
gsub(/<PAGE>/, (@page || 1).to_s)
|
172
|
+
end
|
173
|
+
|
174
|
+
def search(force = false)
|
175
|
+
return @_search if @_search
|
176
|
+
@_search = ""
|
177
|
+
@_search = ((@strip or force) ? clean!(@prefix) + " " : @prefix + " ") if @prefix
|
178
|
+
@_search += ((@strip or force) ? clean!(@search) : @search)
|
179
|
+
end
|
180
|
+
|
181
|
+
def scrape
|
182
|
+
return @cache[@type] if @cache[@type]
|
183
|
+
|
184
|
+
@cache[@type] = []; content[@methods[@type][:selector].to_s].each do |item|
|
185
|
+
item = @methods[@type][:class].new(item.merge(@options))
|
186
|
+
@cache[@type] << item if item.valid?
|
187
|
+
end
|
188
|
+
@cache[@type]
|
189
|
+
end
|
190
|
+
|
191
|
+
def content
|
192
|
+
data = download
|
193
|
+
cd = CharDet.detect(data)
|
194
|
+
data = cd.confidence > 0.6 ? Iconv.conv(cd.encoding, "UTF-8", data) : data
|
195
|
+
@content ||= JSON.parse(data)
|
196
|
+
end
|
197
|
+
|
198
|
+
def download
|
199
|
+
@download ||= RestClient.get(url, :timeout => 10)
|
200
|
+
end
|
201
|
+
|
202
|
+
def errors(error)
|
203
|
+
case error.to_s
|
204
|
+
when "403 Forbidden"
|
205
|
+
raise SpotContainer::RequestLimitError.new(url)
|
206
|
+
else
|
207
|
+
raise error
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def generate_url(type)
|
212
|
+
"http://ws.spotify.com/search/1/#{type}.json?q=<SEARCH>&page=<PAGE>"
|
213
|
+
end
|
214
|
+
end
|