spot 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ spec/run_spec.rb
data/.rspec ADDED
@@ -0,0 +1,5 @@
1
+ --color
2
+ -fs
3
+ -Ilib
4
+ -Ispec
5
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
@@ -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*.
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -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