bassnode-ruby-echonest 0.1.2

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 ADDED
@@ -0,0 +1,5 @@
1
+ pkg
2
+ coverage
3
+ *.gem
4
+ /example*
5
+ *~
data/ChangeLog ADDED
@@ -0,0 +1,4 @@
1
+ == 0.0.1 / 2009-04-14
2
+
3
+ * initial release
4
+
data/README.rdoc ADDED
@@ -0,0 +1,45 @@
1
+ = echonest
2
+
3
+ A Ruby interface for Echo Nest Developer API
4
+
5
+ == Description
6
+
7
+ == Installation
8
+
9
+ === Archive Installation
10
+
11
+ rake install
12
+
13
+ === Gem Installation
14
+
15
+ gem install ruby-echonest
16
+
17
+ == Features/Problems
18
+
19
+ Only supports the API for Track http://developer.echonest.com/docs/v4/track.html
20
+
21
+ == Synopsis
22
+
23
+ require 'rubygems'
24
+ require 'echonest'
25
+
26
+ filename = 'foo.mp3'
27
+ echonest = Echonest('YOUR_API_KEY')
28
+
29
+ analysis = echonest.track.analysis(filename)
30
+ beats = analysis.beats
31
+ segments = analysis.segments
32
+
33
+ # traditional way
34
+ beats = echonest.get_beats(filename)
35
+ segments = echonest.get_segments(filename)
36
+
37
+ == Thanks
38
+
39
+ {koyachi}[http://github.com/koyachi] for original idea http://gist.github.com/87086
40
+
41
+ == Copyright
42
+
43
+ Author:: youpy <youpy@buycheapviagraonlinenow.com>
44
+ Copyright:: Copyright (c) 2009 youpy
45
+ License:: MIT
data/Rakefile ADDED
@@ -0,0 +1,36 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/clean'
4
+ require 'rake/testtask'
5
+ require 'rake/packagetask'
6
+ require 'rake/gempackagetask'
7
+ require 'rake/rdoctask'
8
+ require 'rake/contrib/sshpublisher'
9
+ require 'spec/rake/spectask'
10
+ require 'fileutils'
11
+ include FileUtils
12
+
13
+ $LOAD_PATH.unshift "lib"
14
+ require "echonest"
15
+
16
+
17
+ task :default => [:spec]
18
+ task :package => [:clean]
19
+
20
+ Spec::Rake::SpecTask.new do |t|
21
+ t.spec_opts = ['--options', "spec/spec.opts"]
22
+ t.spec_files = FileList['spec/*_spec.rb']
23
+ t.rcov = true
24
+ end
25
+
26
+ spec = eval(File.read("bassnode-ruby-echonest.gemspec"))
27
+ Rake::GemPackageTask.new(spec) do |p|
28
+ p.need_tar = true
29
+ p.gem_spec = spec
30
+ end
31
+
32
+
33
+ desc "Show information about the gem"
34
+ task :debug_gem do
35
+ puts spec.to_ruby
36
+ end
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{bassnode-ruby-echonest}
5
+ s.version = "0.1.2"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["youpy", "bassnode"]
9
+ s.date = %q{2011-04-24}
10
+ s.description = %q{An Ruby interface for Echo Nest Developer API}
11
+ s.summary = %q{An Ruby interface for Echo Nest Developer API}
12
+ s.email = %q{youpy@buycheapviagraonlinenow.com}
13
+ s.homepage = %q{http://github.com/bassnode/ruby-echonest}
14
+ s.rubyforge_project = %q{bassnode-ruby-echonest}
15
+ s.rubygems_version = %q{1.3.6}
16
+ s.platform = Gem::Platform::RUBY
17
+
18
+ s.require_paths = ["lib"]
19
+ s.extra_rdoc_files = ["README.rdoc", "ChangeLog"]
20
+ s.rdoc_options = ["--title", "ruby-echonest documentation", "--charset", "utf-8", "--opname", "index.html", "--line-numbers", "--main", "README.rdoc", "--inline-source", "--exclude", "^(examples|extras)/"]
21
+ s.files = `git ls-files`.split("\n")
22
+ s.test_files = `git ls-files -- {spec}/*`.split("\n")
23
+
24
+
25
+ s.add_dependency(%q<libxml-ruby>, [">= 0"])
26
+ s.add_dependency(%q<httpclient>, [">= 0"])
27
+ s.add_dependency(%q<hashie>, [">= 0"])
28
+ s.add_development_dependency('rspec')
29
+ end
@@ -0,0 +1,106 @@
1
+ require 'json'
2
+ require 'open-uri'
3
+
4
+ module Echonest
5
+ class Analysis
6
+ CHROMATIC = %w(C C# D D# E F F# G G# A A# B).freeze
7
+
8
+ def initialize(json)
9
+ @body = JSON.parse(json)
10
+ end
11
+
12
+ def self.new_from_url(url)
13
+ new(open(url).read)
14
+ end
15
+
16
+ def tempo
17
+ track_info['tempo']
18
+ end
19
+
20
+ def duration
21
+ track_info['duration']
22
+ end
23
+
24
+ def end_of_fade_in
25
+ track_info['end_of_fade_in']
26
+ end
27
+
28
+ def key
29
+ track_info['key']
30
+ end
31
+
32
+ # Returns the corresponding letter for the key number value.
33
+ def key_letter
34
+ CHROMATIC[key]
35
+ end
36
+
37
+ def loudness
38
+ track_info['loudness']
39
+ end
40
+
41
+ def mode
42
+ track_info['mode']
43
+ end
44
+
45
+ def minor?
46
+ mode == 0
47
+ end
48
+
49
+ def major?
50
+ !minor?
51
+ end
52
+
53
+ def start_of_fade_out
54
+ track_info['start_of_fade_out']
55
+ end
56
+
57
+ def time_signature
58
+ track_info['time_signature']
59
+ end
60
+
61
+ def bars
62
+ @body['bars'].map do |bar|
63
+ Bar.new(bar['start'], bar['duration'], bar['confidence'])
64
+ end
65
+ end
66
+
67
+ def beats
68
+ @body['beats'].map do |beat|
69
+ Beat.new(beat['start'], beat['duration'], beat['confidence'])
70
+ end
71
+ end
72
+
73
+ def sections
74
+ @body['sections'].map do |section|
75
+ Section.new(section['start'], section['duration'], section['confidence'])
76
+ end
77
+ end
78
+
79
+ def tatums
80
+ @body['tatums'].map do |tatum|
81
+ Tatum.new(tatum['start'], tatum['duration'], tatum['confidence'])
82
+ end
83
+ end
84
+
85
+ def segments
86
+ @body['segments'].map do |segment|
87
+ loudness = Loudness.new(0.0, segment['loudness_start'])
88
+ max_loudness = Loudness.new(segment['loudness_max_time'], segment['loudness_max'])
89
+
90
+ Segment.new(
91
+ segment['start'],
92
+ segment['duration'],
93
+ segment['confidence'],
94
+ loudness,
95
+ max_loudness,
96
+ segment['pitches'],
97
+ segment['timbre']
98
+ )
99
+ end
100
+ end
101
+
102
+ def track_info
103
+ @body['track']
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,336 @@
1
+ require 'digest/md5'
2
+ require 'httpclient'
3
+ require 'json'
4
+ # For streaming output
5
+ STDOUT.sync = true
6
+
7
+ module Echonest
8
+ class Api
9
+ VERSION = '4.2'
10
+ BASE_URL = 'http://developer.echonest.com/api/v4/'
11
+ USER_AGENT = '%s/%s' % ['ruby-echonest', ::Echonest::VERSION]
12
+
13
+ include TraditionalApiMethods
14
+
15
+ class Error < StandardError; end
16
+
17
+ attr_reader :user_agent
18
+
19
+ def initialize(api_key)
20
+ @api_key = api_key
21
+ @user_agent = HTTPClient.new(:agent_name => USER_AGENT)
22
+ # for big files
23
+ @user_agent.send_timeout = 60 * 30
24
+ @user_agent.receive_timeout = 60 * 10
25
+ end
26
+
27
+ def track
28
+ ApiMethods::Track.new(self)
29
+ end
30
+
31
+ def artist(name=nil)
32
+ if name
33
+ ApiMethods::Artist.new_from_name(self, name)
34
+ else
35
+ ApiMethods::Artist.new(self)
36
+ end
37
+ end
38
+
39
+ def song
40
+ ApiMethods::Song.new(self)
41
+ end
42
+
43
+ def playlist
44
+ ApiMethods::Playlist.new(self)
45
+ end
46
+
47
+ def default_params
48
+ {
49
+ :format => 'json',
50
+ :api_key => @api_key
51
+ }
52
+ end
53
+
54
+ def build_params(params)
55
+ params = params.
56
+ merge(default_params)
57
+ end
58
+
59
+ def build_params_to_list(params)
60
+ result = []
61
+ hash_to_list = lambda{|kv| [kv[0].to_s, kv[1]]}
62
+ params.each do |param|
63
+ if param.instance_of? Array
64
+ param[1].map do |p1|
65
+ result << [param[0].to_s, p1]
66
+ end
67
+ else
68
+ result << hash_to_list.call(params)
69
+ end
70
+ end
71
+ default_params.each do |kv|
72
+ result << hash_to_list.call(kv) unless params.include? kv[0]
73
+ end
74
+ result
75
+ end
76
+
77
+ def request(name, method, params, file = nil)
78
+ uri = URI.join(BASE_URL, name.to_s)
79
+ if file
80
+ query = build_params(params).sort_by do |param|
81
+ param[0].to_s
82
+ end.inject([]) do |m, param|
83
+ m << [URI.encode(param[0].to_s), URI.encode(param[1])].join('=')
84
+ end.join('&')
85
+
86
+ uri.query = query
87
+ file = file.read unless file.is_a?(String)
88
+ connection = @user_agent.__send__(
89
+ method.to_s + '_async',
90
+ uri,
91
+ file,
92
+ {
93
+ 'Content-Type' => 'application/octet-stream'
94
+ })
95
+
96
+ # Show some feedback for big ole' POSTs
97
+ n=0
98
+ print "8"
99
+ begin
100
+ sleep 2
101
+ n+=2
102
+ print (n%6==0 ? "D 8" : "=")
103
+ end while !connection.finished?
104
+
105
+ res = connection.pop
106
+ response_body = res.content.read
107
+ else
108
+ response_body = @user_agent.__send__(
109
+ method.to_s + '_content',
110
+ uri,
111
+ build_params_to_list(params))
112
+ end
113
+
114
+ response = Response.new(response_body)
115
+ unless response.success?
116
+ raise Error.new(response.status.message)
117
+ end
118
+
119
+ response
120
+ rescue HTTPClient::BadResponseError => e
121
+ raise Error.new('%s: %s' % [name, e.message])
122
+ end
123
+ end
124
+
125
+ module ApiMethods
126
+ class Base
127
+ def initialize(api)
128
+ @api = api
129
+ end
130
+
131
+ def request(*args)
132
+ name, http_method, params = args
133
+ @api.request(name, http_method, params)
134
+ end
135
+
136
+ class << self
137
+ def method_with_required_any(category, method_id, http_method, required, required_any, option, proc, block=nil)
138
+ unless block
139
+ block = Proc.new {|response| response.body}
140
+ end
141
+ method = :request
142
+ required ||= %w[api_key]
143
+ define_method(method_id) do |*args|
144
+ name = "#{category.downcase}/#{method_id.to_s}"
145
+ if args.length > 0
146
+ param_required = {}
147
+ required.each do |k|
148
+ k = k.to_sym
149
+ param_required[k] = args[0].delete(k) if args[0][k]
150
+ end
151
+ param_option = args[0]
152
+ end
153
+ params = ApiMethods::Base.validator(required, required_any, option).call(
154
+ :required => param_required,
155
+ :required_any => proc.call(self),
156
+ :option => param_option)
157
+ block.call(send(method, name, http_method, params))
158
+ end
159
+ end
160
+
161
+ def method_with_option(id, option, &block)
162
+ unless block
163
+ block = Proc.new {|response| response.body}
164
+ end
165
+ required = %w[]
166
+ required_any = %w[]
167
+ method = :request
168
+ http_method = :get
169
+ define_method(id) do |*args|
170
+ name = "#{self.class.to_s.split('::')[-1].downcase}/#{id.to_s}"
171
+ block.call(send(method, name, http_method, ApiMethods::Base.validator(required, required_any, option).call(
172
+ :option => args.length > 0 ? args[0] : {})))
173
+
174
+ end
175
+ end
176
+
177
+ def validator(required, required_any, option)
178
+ Proc.new do |args|
179
+ ApiMethods::Base.build_params_with_validation(args, required, required_any, option)
180
+ end
181
+ end
182
+
183
+ def build_params_with_validation(args, required, required_any, option)
184
+ options = {}
185
+ # api_key is common parameter.
186
+ required -= %w[api_key]
187
+ required.each do |name|
188
+ name = name.to_sym
189
+ raise ArgumentError.new("%s is required" % name) unless args[:required][name]
190
+ options[name] = args[:required][name]
191
+ end
192
+ if required_any.length > 0
193
+ unless required_any.inject(false){|r,i| r || args[:required_any].include?(i.to_sym)}
194
+ raise ArgumentError.new("%s is required" % required_any.join(' or '))
195
+ end
196
+ key = required_any.find {|name| args[:required_any].include?(name.to_sym)}
197
+ options[key.to_sym] = args[:required_any][key.to_sym] if key
198
+ end
199
+ if args[:option] && !args[:option].empty?
200
+ option.each do |name|
201
+ name = name.to_sym
202
+ options[name] = args[:option][name] if args[:option][name]
203
+ end
204
+ end
205
+ options
206
+ end
207
+ end
208
+ end
209
+
210
+ class Track < Base
211
+ def profile(options)
212
+ @api.request('track/profile',
213
+ :get,
214
+ options.merge(:bucket => 'audio_summary'))
215
+ end
216
+
217
+ def analyze(options)
218
+ @api.request('track/analyze',
219
+ :post,
220
+ options.merge(:bucket => 'audio_summary'))
221
+ end
222
+
223
+ def upload(options)
224
+ options.update(:bucket => 'audio_summary')
225
+
226
+ if options.has_key?(:filename)
227
+ filename = options.delete(:filename)
228
+ filetype = filename.match(/\.(mp3|au|ogg)$/)[1]
229
+
230
+ open(filename) do |f|
231
+ @api.request('track/upload',
232
+ :post,
233
+ options.merge(:filetype => filetype),
234
+ f)
235
+ end
236
+ else
237
+ @api.request('track/upload', :post, options)
238
+ end
239
+ end
240
+
241
+ def analysis(filename)
242
+ analysis_url = analysis_url(filename)
243
+ Analysis.new_from_url(analysis_url)
244
+ end
245
+
246
+ def analysis_url(filename)
247
+ md5 = Digest::MD5.hexdigest(open(filename).read)
248
+
249
+ while true
250
+ begin
251
+ response = profile(:md5 => md5)
252
+ rescue Api::Error => e
253
+ if e.message =~ /^The Identifier specified does not exist/
254
+ response = upload(:filename => filename)
255
+ else
256
+ raise
257
+ end
258
+ end
259
+
260
+ case response.body.track.status
261
+ when 'unknown'
262
+ upload(:filename => filename)
263
+ when 'pending'
264
+ sleep 60
265
+ when 'complete'
266
+ return response.body.track.audio_summary.analysis_url
267
+ when 'error'
268
+ raise Error.new(response.body.track.status)
269
+ when 'unavailable'
270
+ analyze(:md5 => md5)
271
+ end
272
+
273
+ sleep 5
274
+ end
275
+ end
276
+ end
277
+
278
+ class Artist < Base
279
+ class << self
280
+ def new_from_name(echonest, artist_name)
281
+ instance = new(echonest)
282
+ instance.artist_name = artist_name
283
+ instance
284
+ end
285
+
286
+ def method_with_artist_id(method_id, option, &block)
287
+ required_any = %w[id name]
288
+ http_method = :get
289
+ proc = lambda {|s| s.artist_name ? {:name => s.artist_name} : {:id => s.artist_id} }
290
+ method_with_required_any('artist', method_id, http_method, [], required_any, option, proc, block)
291
+ end
292
+ end
293
+
294
+ attr_accessor :artist_name, :artist_id
295
+
296
+ method_with_artist_id(:audio, %w[format results start])
297
+ method_with_artist_id(:biographies, %w[format results start license])
298
+ method_with_artist_id(:blogs, %w[format results start])
299
+ method_with_artist_id(:familiarity, %w[format results start])
300
+ method_with_artist_id(:hotttnesss, %w[format results start])
301
+ method_with_artist_id(:images, %w[format results start license])
302
+ method_with_artist_id(:news, %w[format results start])
303
+ method_with_artist_id(:profile, %w[format results start bucket])
304
+ method_with_artist_id(:reviews, %w[format results start])
305
+ method_with_option(:search, %w[format results bucket limit name description fuzzy_match max_familiarity min_familiarity max_hotttnesss min_hotttnesss sort])
306
+ method_with_artist_id(:songs, %w[format results bucket limit])
307
+ method_with_artist_id(:similar, %w[format results start bucket max_familiarity min_familiarity max_hotttnesss min_hotttnesss reverse limit])
308
+ method_with_artist_id(:terms, %w[format sort])
309
+ method_with_option(:top_hottt, %w[format results start bucket limit type])
310
+ method_with_option(:top_terms, %w[format results])
311
+ method_with_artist_id(:urls, %w[format])
312
+ method_with_artist_id(:video, %w[format results start])
313
+ end
314
+
315
+ class Song < Base
316
+ method_with_option(:search, %w[format title artist combined description artist_id results max_tempo min_tempo max_duration min_duration max_loudness min_loudness max_familiarity min_familiarity max_hotttnesss min_hotttnesss min_longitude max_longitude min_latitude max_latitude mode key bucket sort limitt])
317
+ method_with_required_any('song', :profile, :get, %w[api_key id], [], %w[format bucket limit], lambda{})
318
+ # method_with_option(:identify, %w[query code artist title release duration genre bucket])
319
+ def identify(opts)
320
+ file = opts.delete(:code)
321
+ @api.request('song/identify', :post, opts, file).body
322
+ end
323
+ end
324
+
325
+ class Playlist < Base
326
+ method_with_option(:static, %w[format type artist_pick variety artist_id artist song_id description results max_tempo min_tempo max_duration min_duration max_loudness min_loudness artist_max_familiarity artist_min_familiarity artist_max_hotttnesss artist_min_hotttnesss song_max_hotttnesss song_min_hotttnesss artist_min_longitude aritst_max_longitude artist_min_latitude arist_max_latitude mode key bucket sort limit audio])
327
+ method_with_option(:dynamic, %w[format type artist_pick variety artist_id artist song_id description results max_tempo min_tempo max_duration min_duration max_loudness min_loudness artist_max_familiarity artist_min_familiarity artist_max_hotttnesss artist_min_hotttnesss song_max_hotttnesss song_min_hotttnesss artist_min_longitude aritst_max_longitude artist_min_latitude arist_max_latitude mode key bucket sort limit audio session_id dmca rating chain_xspf])
328
+ end
329
+ end
330
+ end
331
+
332
+ class HTTPClient
333
+ def agent_name
334
+ @session_manager.agent_name
335
+ end
336
+ end
@@ -0,0 +1,2 @@
1
+ class Bar < Section
2
+ end
@@ -0,0 +1,2 @@
1
+ class Beat < Section
2
+ end
@@ -0,0 +1,8 @@
1
+ class Loudness
2
+ attr_reader :time, :value
3
+
4
+ def initialize(time, value)
5
+ @time = time
6
+ @value = value
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ class Section
2
+ attr_reader :start, :duration, :confidence
3
+
4
+ def initialize(start, duration, confidence)
5
+ @start = start
6
+ @duration = duration
7
+ @confidence = confidence
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ class Segment < Section
2
+ attr_reader :loudness, :max_loudness, :pitches, :timbre
3
+
4
+ def initialize(start, duration, confidence, loudness, max_loudness, pitches, timbre)
5
+ super(start, duration, confidence)
6
+
7
+ @loudness = loudness
8
+ @max_loudness = max_loudness
9
+ @pitches = pitches
10
+ @timbre = timbre
11
+ end
12
+ end
@@ -0,0 +1,2 @@
1
+ class Tatum < Section
2
+ end
@@ -0,0 +1,41 @@
1
+ require 'json'
2
+ require 'hashie'
3
+
4
+ module Echonest
5
+ class Response
6
+ attr_reader :json
7
+
8
+ def initialize(body)
9
+ @json = Hashie::Mash.new(JSON.parse(body))
10
+ end
11
+
12
+ def status
13
+ @status ||= Status.new(body)
14
+ end
15
+
16
+ def success?
17
+ status.code == Status::SUCCESS
18
+ end
19
+
20
+ def body
21
+ json.response
22
+ end
23
+
24
+ class Status
25
+ UNKNOWN_ERROR = -1
26
+ SUCCESS = 0
27
+ INVALID_API_KEY = 1
28
+ PERMISSION_DENIED = 2
29
+ RATE_LIMIT_EXCEEDED = 3
30
+ MISSING_PARAMETER = 4
31
+ INVALID_PARAMETER = 5
32
+
33
+ attr_reader :code, :message
34
+
35
+ def initialize(response_body)
36
+ @code = response_body.status.code
37
+ @message = response_body.status.message
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,14 @@
1
+ module Echonest
2
+ module TraditionalApiMethods
3
+ def self.included(c)
4
+ %w/tempo duration end_of_fade_in key loudness mode start_of_fade_out time_signature bars beats sections tatums segments/.
5
+ each do |method|
6
+ c.module_eval %Q{
7
+ def get_%s(filename)
8
+ track.analysis(filename).%s
9
+ end
10
+ } % [method, method]
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ module Echonest
2
+ VERSION = '0.1.2'
3
+ end
data/lib/echonest.rb ADDED
@@ -0,0 +1,16 @@
1
+ require 'echonest/version'
2
+ require 'echonest/traditional_api_methods'
3
+ require 'echonest/api'
4
+ require 'echonest/analysis'
5
+ require 'echonest/response'
6
+ require 'echonest/element/section'
7
+ require 'echonest/element/bar'
8
+ require 'echonest/element/beat'
9
+ require 'echonest/element/segment'
10
+ require 'echonest/element/loudness'
11
+ require 'echonest/element/tatum'
12
+
13
+ def Echonest(api_key) Echonest::Api.new(api_key) end
14
+
15
+ module Echonest
16
+ end