ruby-echonest 0.0.6 → 0.1.1

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