mr_eko 0.2.4.1 → 0.3.0
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/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
|