mr_eko 0.2.4.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -2
- data/README.md +2 -2
- data/Rakefile +2 -2
- data/TODO +1 -0
- data/bin/mreko +23 -17
- data/ext/enmfp/README +23 -11
- data/ext/enmfp/RELEASE_NOTES +3 -3
- data/ext/enmfp/codegen.Darwin +0 -0
- data/ext/enmfp/codegen.Linux-i686 +0 -0
- data/ext/enmfp/codegen.Linux-x86_64 +0 -0
- data/ext/enmfp/codegen.windows.exe +0 -0
- data/ext/enmfp/old/codegen.Darwin +0 -0
- data/ext/enmfp/old/codegen.Linux-i686 +0 -0
- data/ext/enmfp/old/codegen.Linux-x86_64 +0 -0
- data/ext/enmfp/old/codegen.windows.exe +0 -0
- data/lib/mr_eko/ext/numeric.rb +11 -0
- data/lib/mr_eko/ext/object.rb +5 -0
- data/lib/mr_eko/playlist.rb +57 -31
- data/lib/mr_eko/presets.rb +4 -4
- data/lib/mr_eko/song.rb +181 -85
- data/lib/mr_eko/timed_playlist.rb +149 -0
- data/lib/mr_eko.rb +40 -5
- data/mr_eko.gemspec +16 -4
- data/test/mr_eko_test.rb +40 -11
- data/test/playlist_test.rb +85 -78
- data/test/song_test.rb +158 -0
- data/test/test.rb +36 -2
- data/test/timed_playlist_test.rb +130 -0
- metadata +71 -24
data/lib/mr_eko/song.rb
CHANGED
@@ -1,109 +1,180 @@
|
|
1
|
+
# TODO: Refactor this so everything's not in class methods
|
1
2
|
class MrEko::Song < Sequel::Model
|
2
3
|
include MrEko::Core
|
3
4
|
plugin :validation_helpers
|
4
5
|
many_to_many :playlists
|
5
6
|
|
6
|
-
|
7
|
-
# but could try uploading a sample of the song (faster).
|
8
|
-
# ffmpeg -y -i mogwai.mp3 -ar 22050 -ac 1 -ss 30 -t 30 output.mp3
|
9
|
-
# or
|
10
|
-
# sox mogwai.mp3 output.mp3 30 60
|
7
|
+
REQUIRED_ID3_TAGS = [:artist, :title]
|
11
8
|
|
12
|
-
|
13
|
-
# of sidestepping the mp3 upload process.
|
14
|
-
def self.enmfp_data(filename, md5)
|
15
|
-
unless File.exists?(fp_location(md5))
|
16
|
-
log 'Running ENMFP'
|
17
|
-
`#{File.join(MrEko::HOME_DIR, 'ext', 'enmfp', enmfp_binary)} "#{File.expand_path(filename)}" > #{fp_location(md5)}`
|
18
|
-
end
|
9
|
+
class EnmfpError < Exception; end
|
19
10
|
|
20
|
-
|
21
|
-
|
11
|
+
# A wrapper which gets called by the bin file.
|
12
|
+
# By default will try to extract the needed song info from the ID3 tags and
|
13
|
+
# if fails, will analyze via ENMFP/upload.
|
14
|
+
#
|
15
|
+
# @param [String] file path of the MP3
|
16
|
+
# @param [Hash] options hash
|
17
|
+
# @option options [Boolean] :tags_only If passed, skip ENMFP
|
18
|
+
# @return [MrEko::Song]
|
19
|
+
def self.create_from_file!(filename, opts={})
|
20
|
+
md5 = MrEko.md5(filename)
|
21
|
+
existing = where(:md5 => md5).first
|
22
|
+
return existing unless existing.nil?
|
23
|
+
|
24
|
+
if song = catalog_via_tags(filename, :md5 => md5)
|
25
|
+
song
|
26
|
+
elsif !opts[:tags_only]
|
27
|
+
catalog_via_enmfp(filename, :md5 => md5)
|
28
|
+
end
|
22
29
|
|
23
|
-
# Return the file path of the EN fingerprint JSON file
|
24
|
-
def self.fp_location(md5)
|
25
|
-
File.expand_path File.join(MrEko::FINGERPRINTS_DIR, "#{md5}.json")
|
26
30
|
end
|
27
31
|
|
28
|
-
#
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
32
|
+
# Run local analysis (ENMFP) on the passed file, send that identifier code
|
33
|
+
# to EN and store the returned details in our DB.
|
34
|
+
# If the local analysis fails, upload the MP3 to EN for server-side analysis.
|
35
|
+
#
|
36
|
+
# @param [String] location of the audio file
|
37
|
+
# @param [Hash] opts
|
38
|
+
# @option opts [String] :md5 pre-calculated MD5 of file
|
39
|
+
# @return [MrEko::Song] the created Song
|
40
|
+
def self.catalog_via_enmfp(filename, opts={})
|
41
|
+
md5 = opts[:md5] || MrEko.md5(filename)
|
42
|
+
fingerprint_json = enmfp_data(filename, md5)
|
43
|
+
|
44
|
+
if fingerprint_json.keys.include?('error')
|
45
|
+
raise EnmfpError, "Errors returned in the ENMFP fingerprint data: #{fingerprint_json.error.inspect}"
|
37
46
|
else
|
38
|
-
|
47
|
+
begin
|
48
|
+
log "Identifying with ENMFP code"
|
49
|
+
|
50
|
+
identify_options = {}.tap do |opts|
|
51
|
+
opts[:code] = fingerprint_json.raw_data
|
52
|
+
opts[:artist] = fingerprint_json.metadata.artist
|
53
|
+
opts[:title] = fingerprint_json.metadata.title
|
54
|
+
opts[:release] = fingerprint_json.metadata.release
|
55
|
+
opts[:bucket] = 'audio_summary'
|
56
|
+
end
|
57
|
+
|
58
|
+
profile = MrEko.nest.song.identify(identify_options)
|
59
|
+
|
60
|
+
raise EnmfpError, "Nothing returned" if profile.songs.empty?
|
61
|
+
profile = profile.songs.first
|
62
|
+
|
63
|
+
# Get the extended audio data from the profile
|
64
|
+
analysis = MrEko.nest.song.profile(:id => profile.id, :bucket => 'audio_summary').songs.first.audio_summary
|
65
|
+
rescue Exception => e
|
66
|
+
log %Q{Issues using ENMFP data "(#{e})" #{e.backtrace.join("\n")}}
|
67
|
+
analysis, profile = get_datapoints_by_upload(filename)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
create do |song|
|
72
|
+
song.filename = File.expand_path(filename)
|
73
|
+
song.md5 = md5
|
74
|
+
song.code = fingerprint_json.code
|
75
|
+
song.tempo = analysis.tempo
|
76
|
+
song.duration = analysis.duration
|
77
|
+
song.fade_in = analysis.end_of_fade_in
|
78
|
+
song.fade_out = analysis.start_of_fade_out
|
79
|
+
song.key = analysis.key
|
80
|
+
song.mode = analysis.mode
|
81
|
+
song.loudness = analysis.loudness
|
82
|
+
song.time_signature = analysis.time_signature
|
83
|
+
song.echonest_id = profile.id
|
84
|
+
song.bitrate = profile.bitrate
|
85
|
+
song.title = profile.title
|
86
|
+
song.artist = profile.artist || profile.artist_name
|
87
|
+
song.album = profile.release
|
88
|
+
song.danceability = profile.audio_summary? ? profile.audio_summary.danceability : analysis.danceability
|
89
|
+
song.energy = profile.audio_summary? ? profile.audio_summary.energy : analysis.energy
|
39
90
|
end
|
40
91
|
end
|
41
92
|
|
42
|
-
#
|
43
|
-
|
44
|
-
|
45
|
-
|
93
|
+
# Parses the file's ID3 tags and converts and strange encoding.
|
94
|
+
#
|
95
|
+
# @param [String] The file path
|
96
|
+
# @return [ID3Lib::Tag]
|
97
|
+
def self.parse_id3_tags(filename)
|
98
|
+
log "Parsing ID3 tags"
|
46
99
|
|
47
|
-
|
100
|
+
clean_tags ID3Lib::Tag.new(filename, ID3Lib::V_ALL)
|
48
101
|
end
|
49
102
|
|
50
|
-
# TODO: Cleanup - This method is prety ugly now.
|
51
|
-
def self.create_from_file!(filename)
|
52
|
-
md5 = MrEko.md5(filename)
|
53
|
-
existing = where(:md5 => md5).first
|
54
|
-
return existing unless existing.nil?
|
55
103
|
|
56
|
-
|
57
|
-
|
104
|
+
# Uses ID3 tags to query Echonest and then store the resultant data.
|
105
|
+
#
|
106
|
+
# @see Song.catalog_via_enmfp for options
|
107
|
+
# @return [MrEko::Song]
|
108
|
+
def self.catalog_via_tags(filename, opts={})
|
109
|
+
tags = parse_id3_tags(filename)
|
110
|
+
return unless has_required_tags? tags
|
111
|
+
|
112
|
+
md5 = opts[:md5] || MrEko.md5(filename)
|
113
|
+
analysis = MrEko.nest.song.search(:artist => tags.artist,
|
114
|
+
:title => tags.title,
|
115
|
+
:bucket => 'audio_summary',
|
116
|
+
:limit => 1).songs.first
|
117
|
+
|
118
|
+
create do |song|
|
119
|
+
song.filename = File.expand_path(filename)
|
120
|
+
song.md5 = md5
|
121
|
+
song.tempo = analysis.audio_summary.tempo
|
122
|
+
song.duration = analysis.audio_summary.duration
|
123
|
+
song.key = analysis.audio_summary.key
|
124
|
+
song.mode = analysis.audio_summary.mode
|
125
|
+
song.loudness = analysis.audio_summary.loudness
|
126
|
+
song.time_signature = analysis.audio_summary.time_signature
|
127
|
+
song.echonest_id = analysis.id
|
128
|
+
song.title = tags.title
|
129
|
+
song.artist = tags.artist
|
130
|
+
song.danceability = analysis.audio_summary.danceability
|
131
|
+
song.energy = analysis.audio_summary.energy
|
132
|
+
# XXX: Won't have these from tags - worth getting from EN?
|
133
|
+
# song.code = fingerprint_json.code
|
134
|
+
# song.album = album
|
135
|
+
# song.fade_in = analysis.end_of_fade_in
|
136
|
+
# song.fade_out = analysis.start_of_fade_out
|
137
|
+
# XXX: ID3Lib doesn't return these - worth parsing?
|
138
|
+
# song.bitrate = profile.bitrate
|
139
|
+
end if analysis
|
140
|
+
end
|
58
141
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
rescue Exception => e
|
78
|
-
log "Issues using ENMP data, uploading \"(#{e})\""
|
79
|
-
analysis, profile = get_datapoints_by_filename(filename)
|
80
|
-
end
|
81
|
-
end
|
142
|
+
def self.has_required_tags?(tags)
|
143
|
+
found = REQUIRED_ID3_TAGS.inject([]) do |present, meth|
|
144
|
+
present << tags.send(meth)
|
145
|
+
end
|
146
|
+
|
147
|
+
found.compact.size == REQUIRED_ID3_TAGS.size ? true : false
|
148
|
+
end
|
149
|
+
|
150
|
+
# Using the Echonest Musical Fingerprint lib in the hopes
|
151
|
+
# of sidestepping the mp3 upload process.
|
152
|
+
#
|
153
|
+
# @param [String] file path of the MP3
|
154
|
+
# @param [String] MD5 hash of the file
|
155
|
+
# @return [Hash] data from the ENMFP process
|
156
|
+
def self.enmfp_data(filename, md5)
|
157
|
+
unless File.exists?(fp_location(md5))
|
158
|
+
log 'Running ENMFP'
|
159
|
+
`#{File.join(MrEko::HOME_DIR, 'ext', 'enmfp', MrEko.enmfp_binary)} "#{File.expand_path(filename)}" > #{fp_location(md5)}`
|
82
160
|
end
|
83
161
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
song.title = profile.title
|
101
|
-
song.artist = profile.artist || profile.artist_name
|
102
|
-
song.album = profile.release
|
103
|
-
song.danceability = profile.audio_summary? ? profile.audio_summary.danceability : analysis.danceability
|
104
|
-
song.energy = profile.audio_summary? ? profile.audio_summary.energy : analysis.energy
|
105
|
-
|
106
|
-
song.save
|
162
|
+
raw_json = File.read fp_location(md5)
|
163
|
+
hash = Hashie::Mash.new(JSON.parse(raw_json).first)
|
164
|
+
hash.raw_data = raw_json
|
165
|
+
hash
|
166
|
+
end
|
167
|
+
|
168
|
+
# Returns the analysis and profile data from Echonest for the given track.
|
169
|
+
#
|
170
|
+
# @param [String] file path of the MP3
|
171
|
+
# @return [Array] Analysis and profile data from EN
|
172
|
+
def self.get_datapoints_by_upload(filename)
|
173
|
+
log "Uploading data to EN for analysis"
|
174
|
+
analysis = MrEko.nest.track.analysis(filename)
|
175
|
+
profile = MrEko.nest.track.profile(:md5 => MrEko.md5(filename), :bucket => 'audio_summary').body.track
|
176
|
+
|
177
|
+
return [analysis, profile]
|
107
178
|
end
|
108
179
|
|
109
180
|
def validate
|
@@ -117,6 +188,31 @@ class MrEko::Song < Sequel::Model
|
|
117
188
|
self.md5 ||= MrEko.md5(filename)
|
118
189
|
end
|
119
190
|
|
191
|
+
# Return the file path of the EN fingerprint JSON file
|
192
|
+
#
|
193
|
+
# @param [String] MD5 hash of the file
|
194
|
+
# @return [String] full path of file with passed MP5
|
195
|
+
def self.fp_location(md5)
|
196
|
+
File.expand_path File.join(MrEko::FINGERPRINTS_DIR, "#{md5}.json")
|
197
|
+
end
|
198
|
+
|
199
|
+
# @param [ID3Lib::Tag]
|
200
|
+
# @return [ID3Lib::Tag]
|
201
|
+
def self.clean_tags(tags)
|
202
|
+
ic = Iconv.new("utf-8", "ucs-2")
|
203
|
+
|
204
|
+
REQUIRED_ID3_TAGS.each do |rt|
|
205
|
+
decoded = begin
|
206
|
+
ic.iconv(tags.send(rt))
|
207
|
+
rescue Iconv::InvalidCharacter
|
208
|
+
tags.send(rt)
|
209
|
+
end
|
210
|
+
decoded = nil if decoded.blank?
|
211
|
+
tags.send("#{rt}=", decoded)
|
212
|
+
end
|
213
|
+
|
214
|
+
tags
|
215
|
+
end
|
120
216
|
end
|
121
217
|
|
122
218
|
MrEko::Song.plugin :timestamps
|
@@ -0,0 +1,149 @@
|
|
1
|
+
class MrEko::TimedPlaylist
|
2
|
+
|
3
|
+
#
|
4
|
+
attr_reader :songs
|
5
|
+
|
6
|
+
# The number of seconds the playlist should be.
|
7
|
+
attr_reader :length
|
8
|
+
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
# The hash which holds all the controlling parameters for the Playlist.
|
12
|
+
attr_reader :attributes
|
13
|
+
|
14
|
+
# Hash keyed by the attribute w/ a value of the number of steps to reach the
|
15
|
+
# final setting.
|
16
|
+
attr_reader :step_map
|
17
|
+
|
18
|
+
class InvalidAttributes < Exception; end
|
19
|
+
|
20
|
+
|
21
|
+
def initialize(opts={})
|
22
|
+
@attributes = Hash.new{ |hsh, key| hsh[key] = {} }
|
23
|
+
@step_map = Hash.new
|
24
|
+
@songs = []
|
25
|
+
|
26
|
+
handle_opts(opts)
|
27
|
+
|
28
|
+
yield self if block_given?
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
def save
|
33
|
+
validate_attributes
|
34
|
+
determine_steps
|
35
|
+
find_songs
|
36
|
+
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def initial(opt, value)
|
41
|
+
add_attribute(:initial, opt, value)
|
42
|
+
end
|
43
|
+
|
44
|
+
def final(opt, value)
|
45
|
+
add_attribute(:final, opt, value)
|
46
|
+
end
|
47
|
+
|
48
|
+
def static(opt, value)
|
49
|
+
add_attribute(:static, opt, value)
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def handle_opts(opts)
|
56
|
+
@length = opts.delete(:length)
|
57
|
+
@name = opts.delete(:name)
|
58
|
+
end
|
59
|
+
|
60
|
+
def add_attribute(att_type, opt, value)
|
61
|
+
attributes[att_type][opt] = value
|
62
|
+
end
|
63
|
+
|
64
|
+
def validate_attributes
|
65
|
+
init_atts = attributes[:initial]
|
66
|
+
final_atts = attributes[:final]
|
67
|
+
|
68
|
+
unless init_atts.keys.map(&:to_s).sort == final_atts.keys.map(&:to_s).sort
|
69
|
+
raise InvalidAttributes, "You must provide values for both the initial and final settings, not just one."
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def determine_steps
|
74
|
+
|
75
|
+
attributes[:initial].each_pair do |attr, val|
|
76
|
+
|
77
|
+
denominator = case attr
|
78
|
+
when :tempo, :loudness
|
79
|
+
attributes[:final][attr] - attributes[:initial][attr]
|
80
|
+
when :danceability, :energy
|
81
|
+
( ( attributes[:final][attr] - attributes[:initial][attr] ) * 10 ).round
|
82
|
+
when :mode
|
83
|
+
2
|
84
|
+
when :key
|
85
|
+
MrEko.key_lookup(attributes[:final][attr]) - MrEko.key_lookup(attributes[:initial][attr])
|
86
|
+
end
|
87
|
+
|
88
|
+
step_length = @length.to_f / denominator
|
89
|
+
step_length = 4.minutes if step_length.in_minutes < 4
|
90
|
+
|
91
|
+
step_map[attr] = [denominator, step_length.round]
|
92
|
+
end
|
93
|
+
|
94
|
+
step_map
|
95
|
+
end
|
96
|
+
|
97
|
+
# XXX Just sketching this part out at the moment...
|
98
|
+
# needs tests (and complete logic!)
|
99
|
+
def find_songs
|
100
|
+
step_count, step_length = step_map[:tempo]
|
101
|
+
return unless step_count && step_length
|
102
|
+
direction = step_count > 0 ? :asc : :desc
|
103
|
+
sorted_tempos = [attributes[:initial][:tempo], attributes[:final][:tempo]].sort
|
104
|
+
tempo_range = Range.new(*sorted_tempos)
|
105
|
+
all_songs = MrEko::Song.where(:tempo => tempo_range).order("tempo #{direction}".lit).all
|
106
|
+
|
107
|
+
songs_to_examine_per_step = step_count > all_songs.size ? 1 : all_songs.size / step_count
|
108
|
+
|
109
|
+
overall_seconds_used = 0
|
110
|
+
all_songs.each_slice(songs_to_examine_per_step).each do |songs|
|
111
|
+
break if overall_seconds_used >= @length
|
112
|
+
|
113
|
+
song_length_proximity = 0
|
114
|
+
length_map = songs.inject({}) do |hsh, song|
|
115
|
+
song_length_proximity = (song.duration - step_length).abs
|
116
|
+
hsh[song_length_proximity] = song
|
117
|
+
hsh
|
118
|
+
end
|
119
|
+
|
120
|
+
step_seconds_used = 0
|
121
|
+
length_map.sort_by{ |key, song| key }.each do |length, song|
|
122
|
+
@songs << song
|
123
|
+
step_seconds_used += song.duration
|
124
|
+
overall_seconds_used += song.duration
|
125
|
+
break if step_seconds_used >= step_length
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
# Might need to make a cluster map here instead of just choosing enough
|
130
|
+
# songs to fulfill the step_length. This is because the over
|
131
|
+
# Playlist#length can be fulfilled even before we reach the target/final
|
132
|
+
# target. I think a better rule would be to pluck a song having the
|
133
|
+
# initial and final values and then try to evenly spread out the remaining
|
134
|
+
# time with the songs in the middle...hence the map of the clusters of
|
135
|
+
# songs. Then we can make selections more intelliegently.
|
136
|
+
|
137
|
+
@songs
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# @length = 3600 # 1hr
|
142
|
+
# tempo range 20bpm
|
143
|
+
#
|
144
|
+
# get count of all songs with the params, eg: tempo => 120..140
|
145
|
+
# => 100
|
146
|
+
|
147
|
+
# so take 100songs / 20steps = 5 songs per step
|
148
|
+
# out of the first 5 songs, select 3min worth using the first
|
149
|
+
|
data/lib/mr_eko.rb
CHANGED
@@ -7,7 +7,12 @@ require "sequel"
|
|
7
7
|
require "logger"
|
8
8
|
require "hashie"
|
9
9
|
require "digest/md5"
|
10
|
+
require 'id3lib'
|
10
11
|
require "echonest"
|
12
|
+
begin
|
13
|
+
require 'ruby-debug'
|
14
|
+
rescue LoadError
|
15
|
+
end
|
11
16
|
|
12
17
|
STDOUT.sync = true
|
13
18
|
|
@@ -15,12 +20,13 @@ EKO_ENV = ENV['EKO_ENV'] || 'development'
|
|
15
20
|
Sequel.default_timezone = :utc
|
16
21
|
|
17
22
|
module MrEko
|
18
|
-
VERSION = '0.
|
23
|
+
VERSION = '0.3.0'
|
19
24
|
USER_DIR = File.join(ENV['HOME'], ".mreko")
|
20
25
|
FINGERPRINTS_DIR = File.join(USER_DIR, 'fingerprints')
|
26
|
+
LOG_DIR = File.join(USER_DIR, 'logs')
|
21
27
|
HOME_DIR = File.join(File.dirname(__FILE__), '..')
|
22
28
|
|
23
|
-
MODES = %w(minor major)
|
29
|
+
MODES = %w(minor major).freeze
|
24
30
|
CHROMATIC_SCALE = %w(C C# D D# E F F# G G# A A# B).freeze
|
25
31
|
|
26
32
|
class << self
|
@@ -43,15 +49,22 @@ module MrEko
|
|
43
49
|
end
|
44
50
|
|
45
51
|
def setup!
|
46
|
-
@logger ||= Logger.new(STDOUT)
|
47
52
|
setup_directories!
|
53
|
+
setup_logger!
|
48
54
|
setup_db!
|
49
55
|
setup_echonest!
|
50
56
|
end
|
51
57
|
|
58
|
+
# Output to STDOUT in development, otherwise, save to logfile
|
59
|
+
def setup_logger!
|
60
|
+
out = env == 'development' ? STDOUT : File.join(LOG_DIR, "#{env}.log")
|
61
|
+
@logger ||= Logger.new(out)
|
62
|
+
end
|
63
|
+
|
52
64
|
def setup_directories!
|
53
65
|
Dir.mkdir(USER_DIR) unless File.directory?(USER_DIR)
|
54
66
|
Dir.mkdir(FINGERPRINTS_DIR) unless File.directory?(FINGERPRINTS_DIR)
|
67
|
+
Dir.mkdir(LOG_DIR) unless File.directory?(LOG_DIR)
|
55
68
|
end
|
56
69
|
|
57
70
|
def setup_db!
|
@@ -65,7 +78,7 @@ module MrEko
|
|
65
78
|
end
|
66
79
|
|
67
80
|
def db_name
|
68
|
-
env == 'test' ? 'db
|
81
|
+
env == 'test' ? File.join('db', 'eko_test.db') : File.join('db', 'eko.db')
|
69
82
|
end
|
70
83
|
|
71
84
|
def api_key
|
@@ -77,7 +90,7 @@ module MrEko
|
|
77
90
|
|
78
91
|
# Takes 'minor' or 'major' and returns its integer representation.
|
79
92
|
def mode_lookup(mode)
|
80
|
-
MODES.index(mode.downcase)
|
93
|
+
MODES.index(mode.to_s.downcase)
|
81
94
|
end
|
82
95
|
|
83
96
|
# Takes a chromatic key (eg: G#) and returns its integer representation.
|
@@ -89,13 +102,35 @@ module MrEko
|
|
89
102
|
def key_letter(key)
|
90
103
|
CHROMATIC_SCALE[key]
|
91
104
|
end
|
105
|
+
|
106
|
+
# Use the platform-specific binary.
|
107
|
+
def enmfp_binary
|
108
|
+
case ruby_platform
|
109
|
+
when /darwin/
|
110
|
+
'codegen.Darwin'
|
111
|
+
when /686/
|
112
|
+
'codegen.Linux-i686'
|
113
|
+
when /x86/
|
114
|
+
'codegen.Linux-x86_64'
|
115
|
+
else
|
116
|
+
'codegen.windows.exe'
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def ruby_platform
|
121
|
+
RUBY_PLATFORM
|
122
|
+
end
|
123
|
+
|
92
124
|
end
|
93
125
|
end
|
94
126
|
|
95
127
|
|
96
128
|
MrEko.setup!
|
97
129
|
|
130
|
+
require "lib/mr_eko/ext/numeric"
|
131
|
+
require "lib/mr_eko/ext/object"
|
98
132
|
require "lib/mr_eko/core"
|
99
133
|
require "lib/mr_eko/presets"
|
100
134
|
require "lib/mr_eko/playlist"
|
135
|
+
require "lib/mr_eko/timed_playlist"
|
101
136
|
require "lib/mr_eko/song"
|
data/mr_eko.gemspec
CHANGED
@@ -13,8 +13,8 @@ Gem::Specification.new do |s|
|
|
13
13
|
## If your rubyforge_project name is different, then edit it and comment out
|
14
14
|
## the sub! line in the Rakefile
|
15
15
|
s.name = 'mr_eko'
|
16
|
-
s.version = '0.
|
17
|
-
s.date = '2011-
|
16
|
+
s.version = '0.3.0'
|
17
|
+
s.date = '2011-11-30'
|
18
18
|
s.rubyforge_project = 'mr_eko'
|
19
19
|
|
20
20
|
## Make sure your summary is short. The description may be as long
|
@@ -49,15 +49,18 @@ Gem::Specification.new do |s|
|
|
49
49
|
s.add_dependency('sqlite3-ruby', "~> 1.3")
|
50
50
|
s.add_dependency('hashie')
|
51
51
|
s.add_dependency('httpclient', "~> 2.1")
|
52
|
+
s.add_dependency('bassnode-ruby-echonest')
|
52
53
|
s.add_dependency('json', "= 1.4.6")
|
54
|
+
s.add_dependency('id3lib-ruby')
|
53
55
|
|
54
56
|
## List your development dependencies here. Development dependencies are
|
55
57
|
## those that are only needed during development
|
56
58
|
s.add_development_dependency('mocha', "= 0.9.8")
|
57
59
|
s.add_development_dependency('shoulda', "~> 2.11")
|
58
60
|
s.add_development_dependency('test-unit', "~> 2.1")
|
59
|
-
s.add_development_dependency("ruby-debug"
|
60
|
-
|
61
|
+
s.add_development_dependency("ruby-debug")
|
62
|
+
s.add_development_dependency("autotest")
|
63
|
+
|
61
64
|
## Leave this section as-is. It will be automatically generated from the
|
62
65
|
## contents of your Git repository via the gemspec task. DO NOT REMOVE
|
63
66
|
## THE MANIFEST COMMENTS, they are used as delimiters by the task.
|
@@ -79,15 +82,24 @@ Gem::Specification.new do |s|
|
|
79
82
|
ext/enmfp/codegen.Linux-i686
|
80
83
|
ext/enmfp/codegen.Linux-x86_64
|
81
84
|
ext/enmfp/codegen.windows.exe
|
85
|
+
ext/enmfp/old/codegen.Darwin
|
86
|
+
ext/enmfp/old/codegen.Linux-i686
|
87
|
+
ext/enmfp/old/codegen.Linux-x86_64
|
88
|
+
ext/enmfp/old/codegen.windows.exe
|
82
89
|
lib/mr_eko.rb
|
83
90
|
lib/mr_eko/core.rb
|
91
|
+
lib/mr_eko/ext/numeric.rb
|
92
|
+
lib/mr_eko/ext/object.rb
|
84
93
|
lib/mr_eko/playlist.rb
|
85
94
|
lib/mr_eko/presets.rb
|
86
95
|
lib/mr_eko/song.rb
|
96
|
+
lib/mr_eko/timed_playlist.rb
|
87
97
|
mr_eko.gemspec
|
88
98
|
test/mr_eko_test.rb
|
89
99
|
test/playlist_test.rb
|
100
|
+
test/song_test.rb
|
90
101
|
test/test.rb
|
102
|
+
test/timed_playlist_test.rb
|
91
103
|
]
|
92
104
|
# = MANIFEST =
|
93
105
|
|
data/test/mr_eko_test.rb
CHANGED
@@ -1,25 +1,54 @@
|
|
1
|
-
class MrEkoTest < Test::Unit::TestCase
|
1
|
+
class MrEkoTest < Test::Unit::TestCase
|
2
2
|
|
3
3
|
context "the module" do
|
4
|
-
|
4
|
+
|
5
5
|
should "return an Echonest API instance for nest" do
|
6
6
|
assert_instance_of Echonest::Api, MrEko.nest
|
7
7
|
end
|
8
|
-
|
8
|
+
|
9
9
|
should "return a Sequel instance for connection" do
|
10
10
|
assert_instance_of Sequel::SQLite::Database, MrEko.connection
|
11
11
|
end
|
12
|
-
|
13
|
-
# should "raise an error when there is no api.key found" do
|
14
|
-
# File.expects(:exists?).with(File.join(MrEko::USER_DIR, 'echonest_api.key')).returns(false)
|
15
|
-
# File.expects(:exists?).with(File.join(MrEko::HOME_DIR, 'echonest_api.key')).returns(false)
|
16
|
-
# assert_raise(RuntimeError){ MrEko.setup_echonest! }
|
17
|
-
# end
|
18
|
-
|
12
|
+
|
19
13
|
should "return the MD5 of the passed filename" do
|
20
14
|
md5 = Digest::MD5.hexdigest(open(__FILE__).read)
|
21
15
|
assert_equal md5, MrEko.md5(__FILE__)
|
22
16
|
end
|
17
|
+
|
18
|
+
context 'db_name' do
|
19
|
+
|
20
|
+
should 'return the test DB when in that env' do
|
21
|
+
assert_equal 'db/eko_test.db', MrEko.db_name
|
22
|
+
end
|
23
|
+
|
24
|
+
should 'return the main DB when not in the test env' do
|
25
|
+
MrEko.stubs(:env).returns('development')
|
26
|
+
assert_equal 'db/eko.db', MrEko.db_name
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'enmfp_binary' do
|
31
|
+
|
32
|
+
should 'return proper Darwin bin' do
|
33
|
+
MrEko.stubs(:ruby_platform).returns("i686-darwin10.6.0")
|
34
|
+
assert_equal 'codegen.Darwin', MrEko.enmfp_binary
|
35
|
+
end
|
36
|
+
|
37
|
+
should 'return proper Windows bin' do
|
38
|
+
MrEko.stubs(:ruby_platform).returns("Win32")
|
39
|
+
assert_equal 'codegen.windows.exe', MrEko.enmfp_binary
|
40
|
+
end
|
41
|
+
|
42
|
+
should 'return proper 686 bin' do
|
43
|
+
MrEko.stubs(:ruby_platform).returns("i686-linux")
|
44
|
+
assert_equal 'codegen.Linux-i686', MrEko.enmfp_binary
|
45
|
+
end
|
46
|
+
|
47
|
+
should 'return proper x86 bin' do
|
48
|
+
MrEko.stubs(:ruby_platform).returns("x86_64-linux")
|
49
|
+
assert_equal 'codegen.Linux-x86_64', MrEko.enmfp_binary
|
50
|
+
end
|
51
|
+
end
|
23
52
|
end
|
24
53
|
|
25
|
-
end
|
54
|
+
end
|