spot 0.1.4 → 2.0.0

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