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 CHANGED
@@ -1,7 +1,8 @@
1
1
  require 'rubygems'
2
2
  require 'rake'
3
3
  require 'date'
4
- # require "bundler"
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,11 @@
1
+ module MrEko
2
+
3
+ class NoSongsError < Exception; end
4
+
5
+ class EnmfpError < Exception; end
6
+
7
+ class InvalidAttributes < Exception; end
8
+
9
+ end
10
+
11
+
@@ -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
@@ -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
@@ -0,0 +1,4 @@
1
+ class MrEko::PlaylistEntry < Sequel::Model
2
+ many_to_one :playlists
3
+ many_to_one :songs
4
+ end
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
- 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
4
+ attr_accessor :length
13
5
 
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
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
- @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?
13
+ @facet = opts.delete(:facet)
14
+ @length = opts.delete(:length)
29
15
 
16
+ super
30
17
  end
31
18
 
32
- def save
33
- validate_attributes
34
- determine_steps
35
- find_songs
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
- self
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
- def final(opt, value)
45
- add_attribute(:final, opt, value)
46
- end
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
- def static(opt, value)
49
- add_attribute(:static, opt, value)
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 handle_opts(opts)
56
- @length = opts.delete(:length)
57
- @name = opts.delete(:name)
58
- end
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
- def add_attribute(att_type, opt, value)
61
- attributes[att_type][opt] = value
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 validate_attributes
65
- init_atts = attributes[:initial]
66
- final_atts = attributes[:final]
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
- 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."
75
+ group
70
76
  end
77
+
71
78
  end
72
79
 
73
- def determine_steps
80
+ private
74
81
 
75
- attributes[:initial].each_pair do |attr, val|
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
- 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
89
+ # Make sure we're sorted by the facet
90
+ song_group.sort!{ |a,b| a[facet] <=> b[facet] }
87
91
 
88
- step_length = @length.to_f / denominator
89
- step_length = 4.minutes if step_length.in_minutes < 4
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
- step_map[attr] = [denominator, step_length.round]
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
- step_map
95
- end
101
+ diff_cost = diffs.inject(0){ |sum, n| sum + n }.abs
96
102
 
97
- # XXX Just sketching this part out at the moment...
98
- # needs tests and to work with attributes other than tempo!
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} & ~{: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
- # Make sure the songs are added the required order as they have been
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.3.4'
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
- require "mr_eko/ext/numeric"
130
- require "mr_eko/ext/object"
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.3.4'
17
- s.date = '2011-12-02'
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
@@ -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::Playlist::NoSongsError){ MrEko::Playlist.create_from_options(@options) }
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::Song::EnmfpError)
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::Song::EnmfpError.new("no songs"))
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)
@@ -2,14 +2,10 @@ class TimedPlaylistTest < Test::Unit::TestCase
2
2
 
3
3
  context 'a new TimedPlaylist' do
4
4
 
5
- should 'accept a hash of options' do
6
- assert MrEko::TimedPlaylist.new(:length => 600, :name => 'whatever')
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 'Awesome', pl.name
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(:mode, :minor)
22
- assert_equal :minor, pl.attributes[:initial][:mode]
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(:tempo, 120)
32
- assert_equal 120, pl.attributes[:final][:tempo]
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 'populate the step_map' do
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
- should 'populate the step_map with the proper energy and danceability fractional step data' do
95
- list = MrEko::TimedPlaylist.new(:length => 3600) do |pl|
96
- pl.initial(:energy, 0.622)
97
- pl.final(:energy, 0.888)
37
+ # No moar songs!
38
+ MrEko::Song.delete
98
39
 
99
- pl.initial(:danceability, 0.22)
100
- pl.final(:danceability, 0.88)
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
- assert list.save
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(:tempo, 66)
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::TimedPlaylist::InvalidAttributes){ pl.save }
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: 27
4
+ hash: 13
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 3
9
8
  - 4
10
- version: 0.3.4
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: 2011-12-02 00:00:00 Z
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