spot 0.1.4 → 2.0.0

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/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