mr_eko 0.3.4 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +5 -10
- data/db/migrate/05_remove_fades_from_song.rb +15 -0
- data/db/migrate/06_add_song_position.rb +16 -0
- data/lib/mr_eko/exceptions.rb +11 -0
- data/lib/mr_eko/ext/array.rb +24 -0
- data/lib/mr_eko/playlist.rb +2 -6
- data/lib/mr_eko/playlist_entry.rb +4 -0
- data/lib/mr_eko/song.rb +7 -13
- data/lib/mr_eko/timed_playlist.rb +76 -121
- data/lib/mr_eko.rb +6 -3
- data/mr_eko.gemspec +8 -2
- data/test/playlist_test.rb +1 -1
- data/test/song_test.rb +2 -2
- data/test/timed_playlist_test.rb +19 -91
- metadata +23 -4
data/Rakefile
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'rake'
|
3
3
|
require 'date'
|
4
|
-
|
4
|
+
require "yard"
|
5
|
+
|
5
6
|
#############################################################################
|
6
7
|
#
|
7
8
|
# Helper functions
|
@@ -63,20 +64,14 @@ task :coverage do
|
|
63
64
|
sh "open coverage/index.html"
|
64
65
|
end
|
65
66
|
|
66
|
-
require 'rdoc/task'
|
67
|
-
Rake::RDocTask.new do |rdoc|
|
68
|
-
rdoc.rdoc_dir = 'rdoc'
|
69
|
-
rdoc.title = "#{name} #{version}"
|
70
|
-
rdoc.rdoc_files.include('README*')
|
71
|
-
rdoc.rdoc_files.include('lib/**/*.rb')
|
72
|
-
end
|
73
|
-
|
74
67
|
#############################################################################
|
75
68
|
#
|
76
69
|
# Custom tasks (add your own tasks here)
|
77
70
|
#
|
78
71
|
#############################################################################
|
79
|
-
|
72
|
+
YARD::Rake::YardocTask.new do |t|
|
73
|
+
t.files = ['lib/**/*.rb','README.md'] # optional
|
74
|
+
end
|
80
75
|
|
81
76
|
|
82
77
|
#############################################################################
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class AddCodeToSongs < Sequel::Migration
|
2
|
+
def up
|
3
|
+
alter_table(:songs) do
|
4
|
+
drop_column :fade_in
|
5
|
+
drop_column :fade_out
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def down
|
10
|
+
alter_table(:songs) do
|
11
|
+
add_column :fade_in, Float
|
12
|
+
add_column :fade_out, Float
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class AddSongPosition < Sequel::Migration
|
2
|
+
def up
|
3
|
+
alter_table(:playlists_songs) do
|
4
|
+
add_column :position, Integer
|
5
|
+
end
|
6
|
+
|
7
|
+
rename_table :playlists_songs, :playlist_entries
|
8
|
+
end
|
9
|
+
|
10
|
+
def down
|
11
|
+
alter_table(:playlists_songs) do
|
12
|
+
drop_column :position
|
13
|
+
end
|
14
|
+
rename_table :playlist_entries, :playlists_songs
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Array
|
2
|
+
|
3
|
+
# Ripped from ActiveSupport
|
4
|
+
def in_groups_of(number, fill_with = nil)
|
5
|
+
if fill_with == false
|
6
|
+
collection = self
|
7
|
+
else
|
8
|
+
# size % number gives how many extra we have;
|
9
|
+
# subtracting from number gives how many to add;
|
10
|
+
# modulo number ensures we don't add group of just fill.
|
11
|
+
padding = (number - size % number) % number
|
12
|
+
collection = dup.concat([fill_with] * padding)
|
13
|
+
end
|
14
|
+
|
15
|
+
if block_given?
|
16
|
+
collection.each_slice(number) { |slice| yield(slice) }
|
17
|
+
else
|
18
|
+
[].tap do |groups|
|
19
|
+
collection.each_slice(number) { |group| groups << group }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
data/lib/mr_eko/playlist.rb
CHANGED
@@ -3,10 +3,8 @@ class MrEko::Playlist < Sequel::Model
|
|
3
3
|
include MrEko::Core
|
4
4
|
include MrEko::Presets
|
5
5
|
|
6
|
-
class NoSongsError < Exception; end
|
7
|
-
|
8
6
|
plugin :validation_helpers
|
9
|
-
many_to_many :songs
|
7
|
+
many_to_many :songs, :join_table => :playlist_entries, :order => :playlist_entries__position.asc
|
10
8
|
FORMATS = [:pls, :m3u, :text].freeze
|
11
9
|
DEFAULT_OPTIONS = [ {:tempo => 0..500}, {:duration => 10..1200} ].freeze
|
12
10
|
|
@@ -25,7 +23,7 @@ class MrEko::Playlist < Sequel::Model
|
|
25
23
|
songs.each{ |song| pl.add_song(song) }
|
26
24
|
pl.save
|
27
25
|
else
|
28
|
-
raise NoSongsError.new("No songs match those criteria!")
|
26
|
+
raise MrEko::NoSongsError.new("No songs match those criteria!")
|
29
27
|
end
|
30
28
|
end
|
31
29
|
end
|
@@ -128,5 +126,3 @@ class MrEko::Playlist < Sequel::Model
|
|
128
126
|
"TBD"
|
129
127
|
end
|
130
128
|
end
|
131
|
-
|
132
|
-
MrEko::Playlist.plugin :timestamps
|
data/lib/mr_eko/song.rb
CHANGED
@@ -2,12 +2,10 @@
|
|
2
2
|
class MrEko::Song < Sequel::Model
|
3
3
|
include MrEko::Core
|
4
4
|
plugin :validation_helpers
|
5
|
-
many_to_many :playlists
|
5
|
+
many_to_many :playlists, :through => :playlist_entries
|
6
6
|
|
7
7
|
REQUIRED_ID3_TAGS = [:artist, :title]
|
8
8
|
|
9
|
-
class EnmfpError < Exception; end
|
10
|
-
|
11
9
|
# A wrapper which gets called by the bin file.
|
12
10
|
# By default will try to extract the needed song info from the ID3 tags and
|
13
11
|
# if fails, will analyze via ENMFP/upload.
|
@@ -46,7 +44,7 @@ class MrEko::Song < Sequel::Model
|
|
46
44
|
begin
|
47
45
|
fingerprint_json = enmfp_data(filename, md5)
|
48
46
|
profile = identify_from_enmfp_data(fingerprint_json)
|
49
|
-
rescue EnmfpError => e
|
47
|
+
rescue MrEko::EnmfpError => e
|
50
48
|
log %Q{Issues using ENMFP data "(#{e})" #{e.backtrace.join("\n")}}
|
51
49
|
analysis, profile = get_datapoints_by_upload(filename)
|
52
50
|
end
|
@@ -115,13 +113,11 @@ class MrEko::Song < Sequel::Model
|
|
115
113
|
song.echonest_id = analysis.id
|
116
114
|
song.title = tags.title
|
117
115
|
song.artist = tags.artist
|
116
|
+
song.album = tags.album
|
118
117
|
song.danceability = analysis.audio_summary.danceability
|
119
118
|
song.energy = analysis.audio_summary.energy
|
120
119
|
# XXX: Won't have these from tags - worth getting from EN?
|
121
120
|
# song.code = fingerprint_json.code
|
122
|
-
# song.album = album
|
123
|
-
# song.fade_in = analysis.end_of_fade_in
|
124
|
-
# song.fade_out = analysis.start_of_fade_out
|
125
121
|
# XXX: ID3Lib doesn't return these - worth parsing?
|
126
122
|
# song.bitrate = profile.bitrate
|
127
123
|
end if analysis
|
@@ -153,11 +149,11 @@ class MrEko::Song < Sequel::Model
|
|
153
149
|
hash.raw_data = raw_json
|
154
150
|
|
155
151
|
if hash.keys.include?('error')
|
156
|
-
raise EnmfpError, "Errors returned in the ENMFP fingerprint data: #{hash.error.inspect}"
|
152
|
+
raise MrEko::EnmfpError, "Errors returned in the ENMFP fingerprint data: #{hash.error.inspect}"
|
157
153
|
end
|
158
154
|
|
159
155
|
rescue JSON::ParserError => e
|
160
|
-
raise EnmfpError, e.message
|
156
|
+
raise MrEko::EnmfpError, e.message
|
161
157
|
end
|
162
158
|
|
163
159
|
hash
|
@@ -206,7 +202,7 @@ class MrEko::Song < Sequel::Model
|
|
206
202
|
end
|
207
203
|
|
208
204
|
profile = MrEko.nest.song.identify(identify_options)
|
209
|
-
raise EnmfpError, "Nothing returned from song/identify API call" if profile.songs.empty?
|
205
|
+
raise MrEko::EnmfpError, "Nothing returned from song/identify API call" if profile.songs.empty?
|
210
206
|
|
211
207
|
profile.songs.first
|
212
208
|
end
|
@@ -227,7 +223,7 @@ class MrEko::Song < Sequel::Model
|
|
227
223
|
REQUIRED_ID3_TAGS.each do |rt|
|
228
224
|
decoded = begin
|
229
225
|
ic.iconv(tags.send(rt))
|
230
|
-
rescue Iconv::InvalidCharacter
|
226
|
+
rescue Iconv::InvalidCharacter, Iconv::IllegalSequence
|
231
227
|
tags.send(rt)
|
232
228
|
end
|
233
229
|
decoded = nil if decoded.blank?
|
@@ -237,5 +233,3 @@ class MrEko::Song < Sequel::Model
|
|
237
233
|
tags
|
238
234
|
end
|
239
235
|
end
|
240
|
-
|
241
|
-
MrEko::Song.plugin :timestamps
|
@@ -1,154 +1,109 @@
|
|
1
|
-
class MrEko::TimedPlaylist
|
2
|
-
|
3
|
-
#
|
4
|
-
attr_reader :songs
|
1
|
+
class MrEko::TimedPlaylist < MrEko::Playlist
|
5
2
|
|
6
3
|
# The number of seconds the playlist should be.
|
7
|
-
|
8
|
-
|
9
|
-
attr_reader :name
|
10
|
-
|
11
|
-
# The hash which holds all the controlling parameters for the Playlist.
|
12
|
-
attr_reader :attributes
|
4
|
+
attr_accessor :length
|
13
5
|
|
14
|
-
#
|
15
|
-
|
16
|
-
attr_reader :step_map
|
17
|
-
|
18
|
-
class InvalidAttributes < Exception; end
|
6
|
+
# The name of the Song attribute to build on (tempo, key, etc.).
|
7
|
+
attr_accessor :facet
|
19
8
|
|
9
|
+
# The start and end value for the facet
|
10
|
+
attr_accessor :initial, :final
|
20
11
|
|
21
12
|
def initialize(opts={})
|
22
|
-
@
|
23
|
-
@
|
24
|
-
@songs = []
|
25
|
-
|
26
|
-
handle_opts(opts)
|
27
|
-
|
28
|
-
yield self if block_given?
|
13
|
+
@facet = opts.delete(:facet)
|
14
|
+
@length = opts.delete(:length)
|
29
15
|
|
16
|
+
super
|
30
17
|
end
|
31
18
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
19
|
+
# Have to add songs after save due to the Playlist needing to have a primary
|
20
|
+
# key (generated at time of save). Lame.
|
21
|
+
def after_save
|
22
|
+
prepare_attributes
|
23
|
+
find_song_groups!
|
36
24
|
|
37
|
-
|
38
|
-
end
|
39
|
-
|
40
|
-
def initial(opt, value)
|
41
|
-
add_attribute(:initial, opt, value)
|
42
|
-
end
|
25
|
+
songs = @song_groups.sort_by{ |group| cost_of group }.first
|
43
26
|
|
44
|
-
|
45
|
-
|
46
|
-
|
27
|
+
# Sort em
|
28
|
+
direction = final - initial > 0 ? :asc : :desc
|
29
|
+
songs = songs.sort_by(&facet)
|
30
|
+
songs = songs.reverse if direction == :desc
|
47
31
|
|
48
|
-
|
49
|
-
|
32
|
+
songs.each_with_index do |song, position|
|
33
|
+
MrEko::PlaylistEntry.create(:playlist_id => self.id, :song_id => song.id, :position => position)
|
34
|
+
end
|
50
35
|
end
|
51
36
|
|
52
|
-
|
53
37
|
private
|
54
38
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
58
|
-
|
39
|
+
def prepare_attributes
|
40
|
+
unless initial && final
|
41
|
+
raise MrEko::InvalidAttributes, "You must provide values for both the initial and final settings, not just one."
|
42
|
+
end
|
59
43
|
|
60
|
-
|
61
|
-
|
44
|
+
case facet
|
45
|
+
when :danceability, :energy
|
46
|
+
@initial = (intial * 10).round
|
47
|
+
@final = (final * 10).round
|
48
|
+
when :mode
|
49
|
+
@initial = MrEko.mode_lookup(initial)
|
50
|
+
@final = MrEko.mode_lookup(final)
|
51
|
+
when :key
|
52
|
+
@initial = MrEko.key_lookup(initial)
|
53
|
+
@final = MrEko.key_lookup(final)
|
54
|
+
end
|
62
55
|
end
|
63
56
|
|
64
|
-
def
|
65
|
-
|
66
|
-
|
57
|
+
def find_song_groups!(iterations=50)
|
58
|
+
|
59
|
+
sorted = [initial, final].sort
|
60
|
+
# Get every song in the required range
|
61
|
+
all_songs = MrEko::Song.where({facet => Range.new(*sorted)} & ~{:duration => nil}).all
|
62
|
+
raise MrEko::NoSongsError, "no songs with those '#{facet}' parameters" if all_songs.blank?
|
63
|
+
|
64
|
+
# Populate a number of potential playlists
|
65
|
+
@song_groups = Array.new(iterations) do
|
66
|
+
seconds_used = 0
|
67
|
+
group = []
|
68
|
+
until seconds_used >= @length do
|
69
|
+
random_index = rand(all_songs.length - 1)
|
70
|
+
song = all_songs.delete_at(random_index)
|
71
|
+
seconds_used += song.duration
|
72
|
+
group << song
|
73
|
+
end
|
67
74
|
|
68
|
-
|
69
|
-
raise InvalidAttributes, "You must provide values for both the initial and final settings, not just one."
|
75
|
+
group
|
70
76
|
end
|
77
|
+
|
71
78
|
end
|
72
79
|
|
73
|
-
|
80
|
+
private
|
74
81
|
|
75
|
-
|
82
|
+
# How bad is the passed group of songs with respect to the TimedPlaylist's
|
83
|
+
# facet and length contraints?
|
84
|
+
#
|
85
|
+
# @param [Array<MrEko::Song>] the songs
|
86
|
+
# @return [Float] the score - the lower, the better
|
87
|
+
def cost_of(song_group)
|
76
88
|
|
77
|
-
|
78
|
-
|
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
|
89
|
+
# Make sure we're sorted by the facet
|
90
|
+
song_group.sort!{ |a,b| a[facet] <=> b[facet] }
|
87
91
|
|
88
|
-
|
89
|
-
|
92
|
+
first_song_distance_to_target = song_group.first[facet] - initial
|
93
|
+
last_song_distance_to_target = final - song_group.last[facet]
|
90
94
|
|
91
|
-
|
95
|
+
# Calculate the facet differences between each song
|
96
|
+
diffs = []
|
97
|
+
song_group.in_groups_of(2) do |x,y|
|
98
|
+
diffs << ( (x.nil? || y.nil?) ? 0 : x[facet] - y[facet])
|
92
99
|
end
|
93
100
|
|
94
|
-
|
95
|
-
end
|
101
|
+
diff_cost = diffs.inject(0){ |sum, n| sum + n }.abs
|
96
102
|
|
97
|
-
|
98
|
-
|
99
|
-
|
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} & ~{:duration => nil}).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 |step_songs|
|
111
|
-
break if overall_seconds_used >= @length
|
112
|
-
|
113
|
-
song_length_proximity = 0
|
114
|
-
length_map = step_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
|
-
song_set = []
|
122
|
-
length_map.sort_by{ |key, song| key }.each do |length, song|
|
123
|
-
song_set << song
|
124
|
-
step_seconds_used += song.duration
|
125
|
-
overall_seconds_used += song.duration
|
126
|
-
break if step_seconds_used >= step_length
|
127
|
-
end
|
103
|
+
# Penalty if the playlist is 20% longer than it should be
|
104
|
+
total_length = song_group.inject(0.0){ |sum, song| sum + song.duration }
|
105
|
+
length_penalty = total_length / @length > 0.20 ? 1.25 : 1
|
128
106
|
|
129
|
-
|
130
|
-
# sorted by duration and thus may be in an odd order.
|
131
|
-
song_set = direction == :asc ? song_set.sort_by(&:tempo) : song_set.sort_by(&:tempo).reverse
|
132
|
-
@songs = @songs + song_set
|
133
|
-
end
|
134
|
-
# Might need to make a cluster map here instead of just choosing enough
|
135
|
-
# songs to fulfill the step_length. This is because the
|
136
|
-
# Playlist#length can be fulfilled even before we reach the target/final
|
137
|
-
# target. I think a better rule would be to pluck a song having the
|
138
|
-
# initial and final values and then try to evenly spread out the remaining
|
139
|
-
# time with the songs in the middle...hence the map of the clusters of
|
140
|
-
# songs. Then we can make selections more intelliegently.
|
141
|
-
|
142
|
-
@songs
|
107
|
+
(first_song_distance_to_target + last_song_distance_to_target + diff_cost) * length_penalty
|
143
108
|
end
|
144
109
|
end
|
145
|
-
|
146
|
-
# @length = 3600 # 1hr
|
147
|
-
# tempo range 20bpm
|
148
|
-
#
|
149
|
-
# get count of all songs with the params, eg: tempo => 120..140
|
150
|
-
# => 100
|
151
|
-
|
152
|
-
# so take 100songs / 20steps = 5 songs per step
|
153
|
-
# out of the first 5 songs, select 3min worth using the first
|
154
|
-
|
data/lib/mr_eko.rb
CHANGED
@@ -19,7 +19,7 @@ EKO_ENV = ENV['EKO_ENV'] || 'development'
|
|
19
19
|
Sequel.default_timezone = :utc
|
20
20
|
|
21
21
|
module MrEko
|
22
|
-
VERSION = '0.
|
22
|
+
VERSION = '0.4.1'
|
23
23
|
USER_DIR = File.join(ENV['HOME'], ".mreko")
|
24
24
|
FINGERPRINTS_DIR = File.join(USER_DIR, 'fingerprints')
|
25
25
|
LOG_DIR = File.join(USER_DIR, 'logs')
|
@@ -126,10 +126,13 @@ end
|
|
126
126
|
|
127
127
|
MrEko.setup!
|
128
128
|
|
129
|
-
|
130
|
-
require
|
129
|
+
Dir.glob('lib/mr_eko/ext/*.rb').each do |f|
|
130
|
+
require f
|
131
|
+
end
|
132
|
+
require "mr_eko/exceptions"
|
131
133
|
require "mr_eko/core"
|
132
134
|
require "mr_eko/presets"
|
133
135
|
require "mr_eko/playlist"
|
134
136
|
require "mr_eko/timed_playlist"
|
135
137
|
require "mr_eko/song"
|
138
|
+
require "mr_eko/playlist_entry"
|
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 = '
|
16
|
+
s.version = '0.4.1'
|
17
|
+
s.date = '2012-01-01'
|
18
18
|
s.rubyforge_project = 'mr_eko'
|
19
19
|
|
20
20
|
## Make sure your summary is short. The description may be as long
|
@@ -61,6 +61,7 @@ Gem::Specification.new do |s|
|
|
61
61
|
s.add_development_dependency("ruby-debug")
|
62
62
|
s.add_development_dependency("autotest")
|
63
63
|
s.add_development_dependency("pry")
|
64
|
+
s.add_development_dependency("yard")
|
64
65
|
|
65
66
|
## Leave this section as-is. It will be automatically generated from the
|
66
67
|
## contents of your Git repository via the gemspec task. DO NOT REMOVE
|
@@ -76,6 +77,8 @@ Gem::Specification.new do |s|
|
|
76
77
|
db/migrate/002_add_songs.rb
|
77
78
|
db/migrate/003_add_useful_song_fields.rb
|
78
79
|
db/migrate/04_add_code_to_songs.rb
|
80
|
+
db/migrate/05_remove_fades_from_song.rb
|
81
|
+
db/migrate/06_add_song_position.rb
|
79
82
|
ext/enmfp/LICENSE
|
80
83
|
ext/enmfp/README
|
81
84
|
ext/enmfp/RELEASE_NOTES
|
@@ -89,9 +92,12 @@ Gem::Specification.new do |s|
|
|
89
92
|
ext/enmfp/old/codegen.windows.exe
|
90
93
|
lib/mr_eko.rb
|
91
94
|
lib/mr_eko/core.rb
|
95
|
+
lib/mr_eko/exceptions.rb
|
96
|
+
lib/mr_eko/ext/array.rb
|
92
97
|
lib/mr_eko/ext/numeric.rb
|
93
98
|
lib/mr_eko/ext/object.rb
|
94
99
|
lib/mr_eko/playlist.rb
|
100
|
+
lib/mr_eko/playlist_entry.rb
|
95
101
|
lib/mr_eko/presets.rb
|
96
102
|
lib/mr_eko/song.rb
|
97
103
|
lib/mr_eko/timed_playlist.rb
|
data/test/playlist_test.rb
CHANGED
@@ -20,7 +20,7 @@ class PlaylistTest < Test::Unit::TestCase
|
|
20
20
|
|
21
21
|
should "not create a playlist when there no songs found" do
|
22
22
|
assert_equal 0, MrEko::Song.count
|
23
|
-
assert_raise(MrEko::
|
23
|
+
assert_raise(MrEko::NoSongsError){ MrEko::Playlist.create_from_options(@options) }
|
24
24
|
assert_equal @playlist_count, MrEko::Playlist.count
|
25
25
|
end
|
26
26
|
|
data/test/song_test.rb
CHANGED
@@ -77,7 +77,7 @@ class SongTest < Test::Unit::TestCase
|
|
77
77
|
context 'catalog_via_enmfp' do
|
78
78
|
|
79
79
|
should 'try uploading if the ENMFP fingerprint contains errors' do
|
80
|
-
MrEko::Song.stubs(:enmfp_data).raises(MrEko::
|
80
|
+
MrEko::Song.stubs(:enmfp_data).raises(MrEko::EnmfpError)
|
81
81
|
MrEko::Song.expects(:get_datapoints_by_upload).with(TEST_MP3).returns([stub_everything, stub_everything(:audio_summary => stub_everything, :id => 'yu82')])
|
82
82
|
MrEko::Song.catalog_via_enmfp(TEST_MP3)
|
83
83
|
end
|
@@ -90,7 +90,7 @@ class SongTest < Test::Unit::TestCase
|
|
90
90
|
|
91
91
|
should 'try to upload when no songs are returned from the Song#identify call' do
|
92
92
|
MrEko::Song.stubs(:enmfp_data).returns(enmfp_data_stub)
|
93
|
-
MrEko::Song.expects(:identify_from_enmfp_data).with(enmfp_data_stub).raises(MrEko::
|
93
|
+
MrEko::Song.expects(:identify_from_enmfp_data).with(enmfp_data_stub).raises(MrEko::EnmfpError.new("no songs"))
|
94
94
|
MrEko::Song.expects(:get_datapoints_by_upload).returns([stub_everything, stub_everything(:audio_summary => stub_everything, :id => 'yu82')])
|
95
95
|
|
96
96
|
MrEko::Song.catalog_via_enmfp(TEST_MP3)
|
data/test/timed_playlist_test.rb
CHANGED
@@ -2,14 +2,10 @@ class TimedPlaylistTest < Test::Unit::TestCase
|
|
2
2
|
|
3
3
|
context 'a new TimedPlaylist' do
|
4
4
|
|
5
|
-
should '
|
6
|
-
|
7
|
-
end
|
8
|
-
|
9
|
-
should 'set those options as expected' do
|
10
|
-
pl = MrEko::TimedPlaylist.new(:length => 600, :name => 'Awesome')
|
5
|
+
should 'set the passed options as expected' do
|
6
|
+
pl = MrEko::TimedPlaylist.new(:length => 600, :facet => :tempo)
|
11
7
|
assert_equal 600, pl.length
|
12
|
-
assert_equal
|
8
|
+
assert_equal :tempo, pl.facet
|
13
9
|
end
|
14
10
|
|
15
11
|
end
|
@@ -17,9 +13,9 @@ class TimedPlaylistTest < Test::Unit::TestCase
|
|
17
13
|
context 'initial' do
|
18
14
|
|
19
15
|
should 'add the attribute to the list of initial attributes' do
|
20
|
-
MrEko::TimedPlaylist.new(:length => 600, :name => 'sad shit') do |pl|
|
21
|
-
assert pl.initial
|
22
|
-
assert_equal :minor, pl.
|
16
|
+
MrEko::TimedPlaylist.new(:length => 600, :name => 'sad shit', :facet => :mode) do |pl|
|
17
|
+
assert pl.initial = :minor
|
18
|
+
assert_equal :minor, pl.initial
|
23
19
|
end
|
24
20
|
end
|
25
21
|
end
|
@@ -27,103 +23,35 @@ class TimedPlaylistTest < Test::Unit::TestCase
|
|
27
23
|
context 'final' do
|
28
24
|
|
29
25
|
should 'add the attribute to the list of final attributes' do
|
30
|
-
MrEko::TimedPlaylist.new(:length => 200, :name => 'Rock') do |pl|
|
31
|
-
assert pl.final
|
32
|
-
assert_equal 120, pl.
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
context 'static' do
|
38
|
-
|
39
|
-
should 'add the attribute to the list of static attributes' do
|
40
|
-
MrEko::TimedPlaylist.new(:length => 1000, :name => 'Bump') do |pl|
|
41
|
-
assert pl.static(:genre, 'HipHop')
|
42
|
-
assert_equal 'HipHop', pl.attributes[:static][:genre]
|
26
|
+
MrEko::TimedPlaylist.new(:length => 200, :name => 'Rock', :facet => :tempo) do |pl|
|
27
|
+
assert pl.final = 120
|
28
|
+
assert_equal 120, pl.final
|
43
29
|
end
|
44
30
|
end
|
45
31
|
end
|
46
32
|
|
47
33
|
context 'save' do
|
48
34
|
|
49
|
-
should '
|
50
|
-
list = MrEko::TimedPlaylist.new(:length => 360) do |pl|
|
51
|
-
pl.initial(:tempo, 100)
|
52
|
-
pl.final(:tempo, 106)
|
53
|
-
end
|
54
|
-
|
55
|
-
assert list.step_map.empty?
|
56
|
-
assert list.save
|
57
|
-
assert !list.step_map.empty?
|
58
|
-
end
|
59
|
-
|
60
|
-
should 'increase the step length to 4.minutes if value is less than that' do
|
61
|
-
list = MrEko::TimedPlaylist.new(:length => 300) do |pl|
|
62
|
-
pl.initial(:tempo, 60)
|
63
|
-
pl.final(:tempo, 80)
|
64
|
-
end
|
65
|
-
|
66
|
-
assert list.save
|
67
|
-
assert_equal [20, 240], list.step_map[:tempo]
|
68
|
-
end
|
69
|
-
|
70
|
-
should 'populate the step_map with the proper mode step data' do
|
71
|
-
list = MrEko::TimedPlaylist.new(:length => 3060) do |pl|
|
72
|
-
pl.initial(:mode, :major)
|
73
|
-
pl.final(:mode, :minor)
|
74
|
-
end
|
75
|
-
|
76
|
-
assert list.save
|
77
|
-
assert_equal [2, 3060.to_f/2], list.step_map[:mode]
|
78
|
-
end
|
79
|
-
|
80
|
-
should 'populate the step_map with the proper tempo and loudness step data' do
|
81
|
-
list = MrEko::TimedPlaylist.new(:length => 3600) do |pl|
|
82
|
-
pl.initial(:tempo, 60)
|
83
|
-
pl.final(:tempo, 70)
|
84
|
-
|
85
|
-
pl.initial(:loudness, -13)
|
86
|
-
pl.final(:loudness, -9)
|
87
|
-
end
|
88
|
-
|
89
|
-
assert list.save
|
90
|
-
assert_equal [10, 3600.to_f/10], list.step_map[:tempo]
|
91
|
-
assert_equal [4, 3600.to_f/4], list.step_map[:loudness]
|
92
|
-
end
|
35
|
+
should 'raise an exception when there are no songs for the parameters' do
|
93
36
|
|
94
|
-
|
95
|
-
|
96
|
-
pl.initial(:energy, 0.622)
|
97
|
-
pl.final(:energy, 0.888)
|
37
|
+
# No moar songs!
|
38
|
+
MrEko::Song.delete
|
98
39
|
|
99
|
-
|
100
|
-
pl.
|
40
|
+
list = MrEko::TimedPlaylist.new(:length => 360, :facet => :tempo) do |pl|
|
41
|
+
pl.initial = 100
|
42
|
+
pl.final = 106
|
101
43
|
end
|
102
44
|
|
103
|
-
|
104
|
-
assert_equal [3, (3600.to_f/3).round], list.step_map[:energy]
|
105
|
-
assert_equal [7, (3600.to_f/7).round], list.step_map[:danceability]
|
45
|
+
assert_raises(MrEko::NoSongsError){ list.save }
|
106
46
|
end
|
107
47
|
|
108
|
-
should 'populate the step_map with the proper key step data' do
|
109
|
-
list = MrEko::TimedPlaylist.new(:length => 3060) do |pl|
|
110
|
-
pl.initial(:key, 'C#')
|
111
|
-
pl.final(:key, 'A#')
|
112
|
-
end
|
113
|
-
|
114
|
-
step = MrEko.key_lookup('A#') - MrEko.key_lookup('C#')
|
115
|
-
assert list.save
|
116
|
-
assert_equal [step, 3060.to_f/step], list.step_map[:key]
|
117
|
-
end
|
118
|
-
|
119
|
-
|
120
48
|
context 'validation' do
|
121
49
|
should "raise an exception when initial and final attribute keys don't match" do
|
122
|
-
pl = MrEko::TimedPlaylist.new(:length => 1000) do |pl|
|
123
|
-
assert pl.initial
|
50
|
+
pl = MrEko::TimedPlaylist.new(:length => 1000, :facet => :tempo) do |pl|
|
51
|
+
assert pl.initial = 66
|
124
52
|
end
|
125
53
|
|
126
|
-
assert_raise(MrEko::
|
54
|
+
assert_raise(MrEko::InvalidAttributes){ pl.save }
|
127
55
|
end
|
128
56
|
end
|
129
57
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mr_eko
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 13
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
- 3
|
9
8
|
- 4
|
10
|
-
|
9
|
+
- 1
|
10
|
+
version: 0.4.1
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Ed Hickey
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date:
|
18
|
+
date: 2012-01-01 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: sequel
|
@@ -208,6 +208,20 @@ dependencies:
|
|
208
208
|
version: "0"
|
209
209
|
type: :development
|
210
210
|
version_requirements: *id013
|
211
|
+
- !ruby/object:Gem::Dependency
|
212
|
+
name: yard
|
213
|
+
prerelease: false
|
214
|
+
requirement: &id014 !ruby/object:Gem::Requirement
|
215
|
+
none: false
|
216
|
+
requirements:
|
217
|
+
- - ">="
|
218
|
+
- !ruby/object:Gem::Version
|
219
|
+
hash: 3
|
220
|
+
segments:
|
221
|
+
- 0
|
222
|
+
version: "0"
|
223
|
+
type: :development
|
224
|
+
version_requirements: *id014
|
211
225
|
description: Catalogs music file data and exposes a playlist interface
|
212
226
|
email: bassnode@gmail.com
|
213
227
|
executables:
|
@@ -226,6 +240,8 @@ files:
|
|
226
240
|
- db/migrate/002_add_songs.rb
|
227
241
|
- db/migrate/003_add_useful_song_fields.rb
|
228
242
|
- db/migrate/04_add_code_to_songs.rb
|
243
|
+
- db/migrate/05_remove_fades_from_song.rb
|
244
|
+
- db/migrate/06_add_song_position.rb
|
229
245
|
- ext/enmfp/LICENSE
|
230
246
|
- ext/enmfp/README
|
231
247
|
- ext/enmfp/RELEASE_NOTES
|
@@ -239,9 +255,12 @@ files:
|
|
239
255
|
- ext/enmfp/old/codegen.windows.exe
|
240
256
|
- lib/mr_eko.rb
|
241
257
|
- lib/mr_eko/core.rb
|
258
|
+
- lib/mr_eko/exceptions.rb
|
259
|
+
- lib/mr_eko/ext/array.rb
|
242
260
|
- lib/mr_eko/ext/numeric.rb
|
243
261
|
- lib/mr_eko/ext/object.rb
|
244
262
|
- lib/mr_eko/playlist.rb
|
263
|
+
- lib/mr_eko/playlist_entry.rb
|
245
264
|
- lib/mr_eko/presets.rb
|
246
265
|
- lib/mr_eko/song.rb
|
247
266
|
- lib/mr_eko/timed_playlist.rb
|