ruby-echonest 0.0.6 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -12,14 +12,11 @@ A Ruby interface for Echo Nest Developer API
12
12
 
13
13
  === Gem Installation
14
14
 
15
- gem update --system
16
- gem install gemcutter
17
- gem tumble
18
15
  gem install ruby-echonest
19
16
 
20
17
  == Features/Problems
21
18
 
22
- Only supports the API for Track http://developer.echonest.com/pages/overview
19
+ Only supports the API for Track http://developer.echonest.com/docs/v4/track.html
23
20
 
24
21
  == Synopsis
25
22
 
@@ -28,7 +25,14 @@ Only supports the API for Track http://developer.echonest.com/pages/overview
28
25
 
29
26
  filename = 'foo.mp3'
30
27
  echonest = Echonest('YOUR_API_KEY')
31
- echonest.get_beats(filename)
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)
32
36
 
33
37
  == Thanks
34
38
 
data/Rakefile CHANGED
@@ -62,6 +62,7 @@ spec = Gem::Specification.new do |s|
62
62
 
63
63
  s.add_dependency('libxml-ruby')
64
64
  s.add_dependency('httpclient')
65
+ s.add_dependency('hashie')
65
66
  #s.required_ruby_version = '>= 1.8.2'
66
67
 
67
68
  s.files = %w(README.rdoc ChangeLog Rakefile) +
@@ -0,0 +1,91 @@
1
+ require 'json'
2
+ require 'open-uri'
3
+
4
+ module Echonest
5
+ class Analysis
6
+ def initialize(json)
7
+ @body = JSON.parse(json)
8
+ end
9
+
10
+ def self.new_from_url(url)
11
+ new(open(url).read)
12
+ end
13
+
14
+ def tempo
15
+ track_info['tempo']
16
+ end
17
+
18
+ def duration
19
+ track_info['duration']
20
+ end
21
+
22
+ def end_of_fade_in
23
+ track_info['end_of_fade_in']
24
+ end
25
+
26
+ def key
27
+ track_info['key']
28
+ end
29
+
30
+ def loudness
31
+ track_info['loudness']
32
+ end
33
+
34
+ def mode
35
+ track_info['mode']
36
+ end
37
+
38
+ def start_of_fade_out
39
+ track_info['start_of_fade_out']
40
+ end
41
+
42
+ def time_signature
43
+ track_info['time_signature']
44
+ end
45
+
46
+ def bars
47
+ @body['bars'].map do |bar|
48
+ Bar.new(bar['start'], bar['duration'], bar['confidence'])
49
+ end
50
+ end
51
+
52
+ def beats
53
+ @body['beats'].map do |beat|
54
+ Beat.new(beat['start'], beat['duration'], beat['confidence'])
55
+ end
56
+ end
57
+
58
+ def sections
59
+ @body['sections'].map do |section|
60
+ Section.new(section['start'], section['duration'], section['confidence'])
61
+ end
62
+ end
63
+
64
+ def tatums
65
+ @body['tatums'].map do |tatum|
66
+ Tatum.new(tatum['start'], tatum['duration'], tatum['confidence'])
67
+ end
68
+ end
69
+
70
+ def segments
71
+ @body['segments'].map do |segment|
72
+ loudness = Loudness.new(0.0, segment['loudness_start'])
73
+ max_loudness = Loudness.new(segment['loudness_max_time'], segment['loudness_max'])
74
+
75
+ Segment.new(
76
+ segment['start'],
77
+ segment['duration'],
78
+ segment['confidence'],
79
+ loudness,
80
+ max_loudness,
81
+ segment['pitches'],
82
+ segment['timbre']
83
+ )
84
+ end
85
+ end
86
+
87
+ def track_info
88
+ @body['track']
89
+ end
90
+ end
91
+ end
data/lib/echonest/api.rb CHANGED
@@ -1,12 +1,15 @@
1
+ # -*- coding: utf-8 -*-
1
2
  require 'digest/md5'
2
3
  require 'httpclient'
4
+ require 'json'
3
5
 
4
6
  module Echonest
5
7
  class Api
6
- VERSION = '3'
7
- BASE_URL = 'http://developer.echonest.com/api/'
8
+ BASE_URL = 'http://developer.echonest.com/api/v4/'
8
9
  USER_AGENT = '%s/%s' % ['ruby-echonest', ::Echonest::VERSION]
9
10
 
11
+ include TraditionalApiMethods
12
+
10
13
  class Error < StandardError; end
11
14
 
12
15
  attr_reader :user_agent
@@ -16,182 +19,125 @@ module Echonest
16
19
  @user_agent = HTTPClient.new(:agent_name => USER_AGENT)
17
20
  end
18
21
 
19
- def get_bars(filename)
20
- get_analysys(:get_bars, filename) do |analysis|
21
- analysis.map do |bar|
22
- Bar.new(bar.content.to_f, bar['confidence'].to_f)
23
- end
24
- end
22
+ def track
23
+ ApiMethods::Track.new(self)
25
24
  end
26
25
 
27
- def get_beats(filename)
28
- get_analysys(:get_beats, filename) do |analysis|
29
- analysis.map do |beat|
30
- Beat.new(beat.content.to_f, beat['confidence'].to_f)
31
- end
32
- end
26
+ def build_params(params)
27
+ params = params.
28
+ merge(:format => 'json').
29
+ merge(:api_key => @api_key)
33
30
  end
34
31
 
35
- def get_segments(filename)
36
- get_analysys(:get_segments, filename) do |analysis|
37
- analysis.map do |segment|
38
- max_loudness = loudness = nil
32
+ def request(name, method, params, file = nil)
33
+ if file
34
+ query = build_params(params).sort_by do |param|
35
+ param[0].to_s
36
+ end.inject([]) do |m, param|
37
+ m << [URI.encode(param[0].to_s), URI.encode(param[1])].join('=')
38
+ end.join('&')
39
39
 
40
- segment.find('loudness/dB').map do |db|
41
- if db['type'] == 'max'
42
- max_loudness = Loudness.new(db['time'].to_f, db.content.to_f)
43
- else
44
- loudness = Loudness.new(db['time'].to_f, db.content.to_f)
45
- end
46
- end
47
-
48
- pitches = segment.find('pitches/pitch').map do |pitch|
49
- pitch.content.to_f
50
- end
40
+ uri = URI.join(BASE_URL, name.to_s)
41
+ uri.query = query
51
42
 
52
- timbre = segment.find('timbre/coeff').map do |coeff|
53
- coeff.content.to_f
54
- end
55
-
56
- Segment.new(
57
- segment['start'].to_f,
58
- segment['duration'].to_f,
59
- loudness,
60
- max_loudness,
61
- pitches,
62
- timbre
63
- )
64
- end
43
+ response_body = @user_agent.__send__(
44
+ method.to_s + '_content',
45
+ uri,
46
+ file.read,
47
+ {
48
+ 'Content-Type' => 'application/octet-stream'
49
+ })
50
+ else
51
+ response_body = @user_agent.__send__(
52
+ method.to_s + '_content',
53
+ URI.join(BASE_URL, name.to_s),
54
+ build_params(params))
65
55
  end
66
- end
67
56
 
68
- def get_tempo(filename)
69
- get_analysys(:get_tempo, filename) do |analysis|
70
- analysis.first.content.to_f
71
- end
72
- end
73
-
74
- def get_sections(filename)
75
- get_analysys(:get_sections, filename) do |analysis|
76
- analysis.map do |section|
77
- Section.new(
78
- section['start'].to_f,
79
- section['duration'].to_f
80
- )
81
- end
82
- end
83
- end
84
-
85
- def get_duration(filename)
86
- get_analysys(:get_duration, filename) do |analysis|
87
- analysis.first.content.to_f
57
+ response = Response.new(response_body)
58
+ unless response.success?
59
+ raise Error.new(response.status.message)
88
60
  end
89
- end
90
61
 
91
- def get_end_of_fade_in(filename)
92
- get_analysys(:get_end_of_fade_in, filename) do |analysis|
93
- analysis.first.content.to_f
94
- end
62
+ response
63
+ rescue HTTPClient::BadResponseError => e
64
+ raise Error.new('%s: %s' % [name, e.message])
95
65
  end
66
+ end
96
67
 
97
- def get_key(filename)
98
- get_analysys(:get_key, filename) do |analysis|
99
- ValueWithConfidence.new(analysis.first.content.to_i, analysis.first['confidence'].to_f)
68
+ module ApiMethods
69
+ class Base
70
+ def initialize(api)
71
+ @api = api
100
72
  end
101
73
  end
102
74
 
103
- def get_loudness(filename)
104
- get_analysys(:get_loudness, filename) do |analysis|
105
- analysis.first.content.to_f
75
+ class Track < Base
76
+ def profile(options)
77
+ @api.request('track/profile',
78
+ :get,
79
+ options.merge(:bucket => 'audio_summary'))
106
80
  end
107
- end
108
81
 
109
- def get_metadata(filename)
110
- get_analysys(:get_metadata, filename) do |analysis|
111
- analysis.inject({}) do |memo, key|
112
- memo[key.name] = key.content
113
- memo
114
- end
82
+ def analyze(options)
83
+ @api.request('track/analyze',
84
+ :post,
85
+ options.merge(:bucket => 'audio_summary'))
115
86
  end
116
- end
117
87
 
118
- def get_mode(filename)
119
- get_analysys(:get_mode, filename) do |analysis|
120
- ValueWithConfidence.new(analysis.first.content.to_i, analysis.first['confidence'].to_f)
121
- end
122
- end
88
+ def upload(options)
89
+ options.update(:bucket => 'audio_summary')
123
90
 
124
- def get_start_of_fade_out(filename)
125
- get_analysys(:get_start_of_fade_out, filename) do |analysis|
126
- analysis.first.content.to_f
127
- end
128
- end
91
+ if options.has_key?(:filename)
92
+ filename = options.delete(:filename)
93
+ filetype = filename.to_s.match(/\.(mp3|au|ogg)$/)[1]
129
94
 
130
- def get_tatums(filename)
131
- get_analysys(:get_tatums, filename) do |analysis|
132
- analysis.map do |tatum|
133
- Tatum.new(tatum.content.to_f, tatum['confidence'].to_f)
95
+ open(filename) do |f|
96
+ @api.request('track/upload',
97
+ :post,
98
+ options.merge(:filetype => filetype),
99
+ f)
100
+ end
101
+ else
102
+ @api.request('track/upload', :post, options)
134
103
  end
135
104
  end
136
- end
137
105
 
138
- def get_time_signature(filename)
139
- get_analysys(:get_time_signature, filename) do |analysis|
140
- ValueWithConfidence.new(analysis.first.content.to_i, analysis.first['confidence'].to_f)
106
+ def analysis(filename)
107
+ analysis_url = analysis_url(filename)
108
+ Analysis.new_from_url(analysis_url)
141
109
  end
142
- end
143
110
 
144
- def build_params(params)
145
- params = params.
146
- merge(:version => VERSION).
147
- merge(:api_key => @api_key)
148
- end
111
+ def analysis_url(filename)
112
+ md5 = Digest::MD5.hexdigest(open(filename).read)
149
113
 
150
- def get_analysys(method, filename)
151
- get_trackinfo(method, filename) do |response|
152
- yield response.xml.find_first('/response/analysis')
153
- end
154
- end
155
-
156
- def get_trackinfo(method, filename, &block)
157
- content = open(filename).read
158
- md5 = Digest::MD5.hexdigest(content)
159
-
160
- begin
161
- response = request(method, :md5 => md5)
162
-
163
- block.call(response)
164
- rescue Echonest::Api::Error => e
165
- case e.message
166
- when /Analysis not ready/
167
- sleep 20 # wait for serverside analysis
168
- get_trackinfo(method, filename, &block)
169
- when 'Invalid parameter: unknown MD5 file hash'
170
- upload(filename)
171
- sleep 60 # wait for serverside analysis
172
- get_trackinfo(method, filename, &block)
173
- else
174
- raise
175
- end
176
- end
177
- end
178
-
179
- def upload(filename)
180
- open(filename) do |f|
181
- request(:upload, :file => f)
182
- end
183
- end
114
+ while true
115
+ begin
116
+ response = profile(:md5 => md5)
117
+ rescue Api::Error => e
118
+ if e.message =~ /^The Identifier specified does not exist/
119
+ response = upload(:filename => filename)
120
+ else
121
+ raise
122
+ end
123
+ end
184
124
 
185
- def request(name, params)
186
- method = (name == :upload ? 'post' : 'get')
187
- response_body = @user_agent.__send__(method + '_content', URI.join(BASE_URL, name.to_s), build_params(params))
188
- response = Response.new(response_body)
125
+ case response.body.track.status
126
+ when 'unknown'
127
+ upload(:filename => filename)
128
+ when 'pending'
129
+ sleep 60
130
+ when 'complete'
131
+ return response.body.track.audio_summary.analysis_url
132
+ when 'error'
133
+ raise Error.new(response.body.track.status)
134
+ when 'unavailable'
135
+ analyze(:md5 => md5)
136
+ end
189
137
 
190
- unless response.success?
191
- raise Error.new(response.status.message)
138
+ sleep 5
139
+ end
192
140
  end
193
-
194
- response
195
141
  end
196
142
  end
197
143
  end
@@ -1,5 +1,2 @@
1
- class Bar < ValueWithConfidence
2
- def start
3
- value
4
- end
1
+ class Bar < Section
5
2
  end
@@ -1,5 +1,2 @@
1
- class Beat < ValueWithConfidence
2
- def start
3
- value
4
- end
1
+ class Beat < Section
5
2
  end
@@ -1,8 +1,9 @@
1
1
  class Section
2
- attr_reader :start, :duration
2
+ attr_reader :start, :duration, :confidence
3
3
 
4
- def initialize(start, duration)
4
+ def initialize(start, duration, confidence)
5
5
  @start = start
6
6
  @duration = duration
7
+ @confidence = confidence
7
8
  end
8
9
  end
@@ -1,8 +1,8 @@
1
1
  class Segment < Section
2
2
  attr_reader :loudness, :max_loudness, :pitches, :timbre
3
3
 
4
- def initialize(start, duration, loudness, max_loudness, pitches, timbre)
5
- super(start, duration)
4
+ def initialize(start, duration, confidence, loudness, max_loudness, pitches, timbre)
5
+ super(start, duration, confidence)
6
6
 
7
7
  @loudness = loudness
8
8
  @max_loudness = max_loudness
@@ -1,5 +1,2 @@
1
- class Tatum < ValueWithConfidence
2
- def start
3
- value
4
- end
1
+ class Tatum < Section
5
2
  end
@@ -1,23 +1,24 @@
1
- require "xml"
1
+ require 'json'
2
+ require 'hashie'
2
3
 
3
4
  module Echonest
4
5
  class Response
5
- attr_reader :xml
6
+ attr_reader :json
6
7
 
7
8
  def initialize(body)
8
- @xml = XML::Document.string(body)
9
+ @json = Hashie::Mash.new(JSON.parse(body))
9
10
  end
10
11
 
11
12
  def status
12
- @status ||= Status.new(@xml)
13
+ @status ||= Status.new(body)
13
14
  end
14
15
 
15
16
  def success?
16
17
  status.code == Status::SUCCESS
17
18
  end
18
19
 
19
- def query
20
- @query ||= Query.new(@xml)
20
+ def body
21
+ json.response
21
22
  end
22
23
 
23
24
  class Status
@@ -31,23 +32,9 @@ module Echonest
31
32
 
32
33
  attr_reader :code, :message
33
34
 
34
- def initialize(xml)
35
- @code = xml.find('/response/status/code').first.content.to_s.to_i
36
- @message = xml.find('/response/status/message').first.content.to_s
37
- end
38
- end
39
-
40
- class Query
41
- def initialize(xml)
42
- @parameters = {}
43
-
44
- xml.find('/response/query/parameter').each do |parameter|
45
- @parameters[parameter['name'].to_sym] = parameter.content
46
- end
47
- end
48
-
49
- def [](parameter_name)
50
- @parameters[parameter_name]
35
+ def initialize(response_body)
36
+ @code = response_body.status.code
37
+ @message = response_body.status.message
51
38
  end
52
39
  end
53
40
  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
@@ -1,3 +1,3 @@
1
1
  module Echonest
2
- VERSION = '0.0.6'
2
+ VERSION = '0.1.1'
3
3
  end
data/lib/echonest.rb CHANGED
@@ -1,10 +1,11 @@
1
1
  require 'echonest/version'
2
+ require 'echonest/traditional_api_methods'
2
3
  require 'echonest/api'
4
+ require 'echonest/analysis'
3
5
  require 'echonest/response'
4
- require 'echonest/element/value_with_confidence'
6
+ require 'echonest/element/section'
5
7
  require 'echonest/element/bar'
6
8
  require 'echonest/element/beat'
7
- require 'echonest/element/section'
8
9
  require 'echonest/element/segment'
9
10
  require 'echonest/element/loudness'
10
11
  require 'echonest/element/tatum'
@@ -0,0 +1,98 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'spec_helper'
4
+
5
+ include SpecHelper
6
+
7
+ describe Echonest::Analysis do
8
+ before do
9
+ @analysis = Echonest::Analysis.new(open(fixture('analysis.json')).read)
10
+ end
11
+
12
+ it "should have beats" do
13
+ beats = @analysis.beats
14
+
15
+ beats.size.should eql(324)
16
+ beats.first.start.should eql(0.27661)
17
+ beats.first.duration.should eql(0.36476)
18
+ beats.first.confidence.should eql(0.468)
19
+ end
20
+
21
+ it "should have segments" do
22
+ segments = @analysis.segments
23
+ segment = segments.first
24
+
25
+ segments.size.should eql(274)
26
+ segment.start.should eql(0.0)
27
+ segment.duration.should eql(0.43909)
28
+ segment.confidence.should eql(1.0)
29
+ segment.loudness.time.should eql(0.0)
30
+ segment.loudness.value.should eql(-60.0)
31
+ segment.max_loudness.time.should eql(0.11238)
32
+ segment.max_loudness.value.should eql(-33.563)
33
+ segment.pitches.size.should eql(12)
34
+ segment.pitches.first.should eql(0.138)
35
+ segment.timbre.size.should eql(12)
36
+ segment.timbre.first.should eql(11.525)
37
+ end
38
+
39
+ it "should have bars" do
40
+ bars = @analysis.bars
41
+
42
+ bars.size.should eql(80)
43
+ bars.first.start.should eql(1.00704)
44
+ bars.first.duration.should eql(1.48532)
45
+ bars.first.confidence.should eql(0.188)
46
+ end
47
+
48
+ it "should have tempo" do
49
+ @analysis.tempo.should eql(168.460)
50
+ end
51
+
52
+ it "should have sections" do
53
+ sections = @analysis.sections
54
+ section = sections.first
55
+
56
+ sections.size.should eql(7)
57
+ section.start.should eql(0.0)
58
+ section.duration.should eql(20.04271)
59
+ section.confidence.should eql(1.0)
60
+ end
61
+
62
+ it "should have durtion" do
63
+ @analysis.duration.should eql(120.68526)
64
+ end
65
+
66
+ it "should have end of fade in" do
67
+ @analysis.end_of_fade_in.should eql(0.0)
68
+ end
69
+
70
+ it "should have start of fade out" do
71
+ @analysis.start_of_fade_out.should eql(113.557)
72
+ end
73
+
74
+ it "should have key" do
75
+ @analysis.key.should eql(7)
76
+ end
77
+
78
+ it "should have loudness" do
79
+ @analysis.loudness.should eql(-19.140)
80
+ end
81
+
82
+ it "should have mode" do
83
+ @analysis.mode.should eql(1)
84
+ end
85
+
86
+ it "should have time signature" do
87
+ @analysis.time_signature.should eql(4)
88
+ end
89
+
90
+ it "should have tatums" do
91
+ tatums = @analysis.tatums
92
+
93
+ tatums.size.should eql(648)
94
+ tatums.first.start.should eql(0.09469)
95
+ tatums.first.duration.should eql(0.18193)
96
+ tatums.first.confidence.should eql(0.286)
97
+ end
98
+ end