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.
@@ -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