bassnode-ruby-echonest 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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