spot 0.1.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.
- 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
|